tx.py 12 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
"""
Copyright  2016-2019 Maël Azimi <m.a@moul.re>

Silkaj is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Silkaj is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
"""

18
from re import compile, search
Tortue95's avatar
Tortue95 committed
19
import math
20
from time import sleep
Moul's avatar
Moul committed
21
from click import command, option, FloatRange
Tortue95's avatar
Tortue95 committed
22

23
from tabulate import tabulate
24 25
from silkaj.network_tools import ClientInstance, HeadBlock
from silkaj.crypto_tools import check_public_key
Moul's avatar
Moul committed
26
from silkaj.tools import message_exit, CurrencySymbol, coroutine
27 28
from silkaj.auth import auth_method
from silkaj.wot import get_uid_from_pubkey
29 30 31 32 33 34
from silkaj.money import (
    get_sources,
    get_amount_from_pubkey,
    UDValue,
    amount_in_current_base,
)
35
from silkaj.constants import NO_MATCHING_ID, SOURCES_PER_TX
36

37 38 39 40
from duniterpy.api.bma.tx import process
from duniterpy.documents import BlockUID, Transaction
from duniterpy.documents.transaction import OutputSource, Unlock, SIGParameter

41

Moul's avatar
Moul committed
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
@command("tx", help="Send transaction")
@option("--amount", type=FloatRange(0.01), help="Quantitative value")
@option("--amountUD", type=float, help="Relative value")
@option("--allSources", is_flag=True, help="Send all sources")
@option(
    "--output",
    required=True,
    help="Pubkey(s)’ recipients + optional checksum: <pubkey>[!checksum]:[<pubkey>[!checksum]]",
)
@option("--comment", default="", help="Comment")
@option(
    "--outputBackChange",
    help="Pubkey recipient to send the rest of the transaction: <pubkey[!checksum]>",
)
@option("--yes", "-y", is_flag=True, help="Assume yes. Do not prompt confirmation")
@coroutine
async def send_transaction(
    amount, amountud, allsources, output, comment, outputbackchange, yes
):
61 62 63
    """
    Main function
    """
Moul's avatar
Moul committed
64 65
    tx_amount = await transaction_amount(amount, amountud, allsources)
    key = auth_method()
66
    issuer_pubkey = key.pubkey
67

68
    pubkey_amount = await get_amount_from_pubkey(issuer_pubkey)
Moul's avatar
Moul committed
69 70
    if allsources:
        tx_amount = pubkey_amount[0]
Moul's avatar
Moul committed
71 72 73 74
    outputAddresses = output.split(":")
    check_transaction_values(
        comment,
        outputAddresses,
Moul's avatar
Moul committed
75
        outputbackchange,
76
        pubkey_amount[0] < tx_amount * len(outputAddresses),
Moul's avatar
Moul committed
77 78 79 80
        issuer_pubkey,
    )

    if (
Moul's avatar
Moul committed
81
        yes
Moul's avatar
Moul committed
82 83
        or input(
            tabulate(
84
                await transaction_confirmation(
85 86 87 88 89 90
                    issuer_pubkey,
                    pubkey_amount[0],
                    tx_amount,
                    outputAddresses,
                    outputbackchange,
                    comment,
Moul's avatar
Moul committed
91 92 93 94 95 96 97
                ),
                tablefmt="fancy_grid",
            )
            + "\nDo you confirm sending this transaction? [yes/no]: "
        )
        == "yes"
    ):
98
        await handle_intermediaries_transactions(
99
            key, issuer_pubkey, tx_amount, outputAddresses, comment, outputbackchange
Moul's avatar
Moul committed
100
        )
101 102


Moul's avatar
Moul committed
103
async def transaction_amount(amount, amountUD, allSources):
104
    """
Moul's avatar
Moul committed
105 106
    Check command line interface amount option
    Return transaction amount
107
    """
Moul's avatar
Moul committed
108 109 110 111 112 113
    if not (amount or amountUD or allSources):
        message_exit("--amount nor --amountUD nor --allSources is set")
    if amount:
        return amount * 100
    if amountUD:
        return amountUD * await UDValue().ud_value
114

115

Moul's avatar
Moul committed
116 117 118
def check_transaction_values(
    comment, outputAddresses, outputBackChange, enough_source, issuer_pubkey
):
119
    checkComment(comment)
120
    for outputAddress in outputAddresses:
121 122
        if check_public_key(outputAddress, True) is False:
            message_exit(outputAddress)
123 124
    if outputBackChange:
        outputBackChange = check_public_key(outputBackChange, True)
125 126
        if check_public_key(outputBackChange, True) is False:
            message_exit(outputBackChange)
127
    if enough_source:
Moul's avatar
Moul committed
128 129 130
        message_exit(
            issuer_pubkey + " pubkey doesn’t have enough money for this transaction."
        )
131 132


133
async def transaction_confirmation(
134
    issuer_pubkey, pubkey_amount, tx_amount, outputAddresses, outputBackChange, comment
Moul's avatar
Moul committed
135
):
136 137 138
    """
    Generate transaction confirmation
    """
139

Moul's avatar
Moul committed
140
    currency_symbol = await CurrencySymbol().symbol
141
    tx = list()
Moul's avatar
Moul committed
142
    tx.append(
143
        ["pubkey’s balance before tx", str(pubkey_amount / 100) + " " + currency_symbol]
Moul's avatar
Moul committed
144 145 146 147 148 149 150 151 152 153
    )
    tx.append(
        [
            "tx amount (unit)",
            str(tx_amount / 100 * len(outputAddresses)) + " " + currency_symbol,
        ]
    )
    tx.append(
        [
            "tx amount (relative)",
Moul's avatar
Moul committed
154 155 156
            str(round(tx_amount / await UDValue().ud_value, 4))
            + " UD "
            + currency_symbol,
Moul's avatar
Moul committed
157 158 159 160
        ]
    )
    tx.append(
        [
161
            "pubkey’s balance after tx",
Moul's avatar
Moul committed
162 163 164 165 166
            str(((pubkey_amount - tx_amount * len(outputAddresses)) / 100))
            + " "
            + currency_symbol,
        ]
    )
167
    tx.append(["from (pubkey)", issuer_pubkey])
168
    id_from = await get_uid_from_pubkey(issuer_pubkey)
169 170
    if id_from is not NO_MATCHING_ID:
        tx.append(["from (id)", id_from])
171
    for outputAddress in outputAddresses:
172
        tx.append(["to (pubkey)", outputAddress])
173
        id_to = await get_uid_from_pubkey(outputAddress)
174 175
        if id_to is not NO_MATCHING_ID:
            tx.append(["to (id)", id_to])
176 177 178 179 180
    if outputBackChange:
        tx.append(["Backchange (pubkey)", outputBackChange])
        id_backchange = await get_uid_from_pubkey(outputBackChange)
        if id_backchange is not NO_MATCHING_ID:
            tx.append(["Backchange (id)", id_backchange])
181
    tx.append(["comment", comment])
182
    return tx
183

Moul's avatar
Moul committed
184

185 186 187 188 189 190 191 192 193 194 195
async def get_list_input_for_transaction(pubkey, TXamount):
    listinput, amount = await get_sources(pubkey)

    # generate final list source
    listinputfinal = []
    totalAmountInput = 0
    intermediatetransaction = False
    for input in listinput:
        listinputfinal.append(input)
        totalAmountInput += amount_in_current_base(input)
        TXamount -= amount_in_current_base(input)
196 197
        # if more than 40 sources, it's an intermediate transaction
        if len(listinputfinal) >= SOURCES_PER_TX:
198 199 200 201 202 203 204 205 206
            intermediatetransaction = True
            break
        if TXamount <= 0:
            break
    if TXamount > 0 and not intermediatetransaction:
        message_exit("Error: you don't have enough money")
    return listinputfinal, totalAmountInput, intermediatetransaction


207
async def handle_intermediaries_transactions(
208
    key, issuers, AmountTransfered, outputAddresses, Comment="", OutputbackChange=None
Moul's avatar
Moul committed
209
):
210
    client = ClientInstance().client
Tortue95's avatar
Tortue95 committed
211
    while True:
212
        listinput_and_amount = await get_list_input_for_transaction(
213
            issuers, AmountTransfered * len(outputAddresses)
Moul's avatar
Moul committed
214
        )
Moul's avatar
Moul committed
215
        intermediatetransaction = listinput_and_amount[2]
Tortue95's avatar
Tortue95 committed
216

Moul's avatar
Moul committed
217
        if intermediatetransaction:
Tortue95's avatar
Tortue95 committed
218
            totalAmountInput = listinput_and_amount[1]
219 220
            await generate_and_send_transaction(
                key,
Moul's avatar
Moul committed
221 222 223
                issuers,
                totalAmountInput,
                listinput_and_amount,
224
                issuers,
Moul's avatar
Moul committed
225 226
                "Change operation",
            )
227
            sleep(1)  # wait 1 second before sending a new transaction
Tortue95's avatar
Tortue95 committed
228 229

        else:
230 231
            await generate_and_send_transaction(
                key,
Moul's avatar
Moul committed
232 233 234 235 236 237 238
                issuers,
                AmountTransfered,
                listinput_and_amount,
                outputAddresses,
                Comment,
                OutputbackChange,
            )
239
            await client.close()
Moul's avatar
Moul committed
240
            break
Tortue95's avatar
Tortue95 committed
241 242


243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
async def generate_and_send_transaction(
    key,
    issuers,
    amount,
    listinput_and_amount,
    outputAddresses,
    Comment,
    OutputbackChange=None,
):
    """
    Display sent transaction
    Generate, sign, and send transaction document
    """
    intermediate_tx = listinput_and_amount[2]
    if intermediate_tx:
        print("Generate Change Transaction")
    else:
        print("Generate Transaction:")
    print("   - From:    " + issuers)
    if isinstance(outputAddresses, str):
        display_sent_tx(outputAddresses, amount)
    else:
        for outputAddress in outputAddresses:
            display_sent_tx(outputAddress, amount)
        if len(outputAddresses) > 1:
            print("   - Total:   " + str(amount / 100 * len(outputAddresses)))

    client = ClientInstance().client
    transaction = await generate_transaction_document(
        issuers,
        amount,
        listinput_and_amount,
        outputAddresses,
        Comment,
        OutputbackChange,
    )
    transaction.sign([key])
    response = await client(process, transaction.signed_raw())
    if response.status == 200:
        print("Transaction successfully sent.")
    else:
        message_exit(
            "Error while publishing transaction: {0}".format(await response.text())
        )


def display_sent_tx(outputAddress, amount):
    print("   - To:     ", outputAddress, "\n   - Amount: ", amount / 100)


Moul's avatar
Moul committed
293
async def generate_transaction_document(
Moul's avatar
Moul committed
294 295 296 297 298 299 300
    issuers,
    AmountTransfered,
    listinput_and_amount,
    outputAddresses,
    Comment="",
    OutputbackChange=None,
):
301

302
    totalAmountTransfered = AmountTransfered * len(outputAddresses)
303

Tortue95's avatar
Tortue95 committed
304 305 306
    listinput = listinput_and_amount[0]
    totalAmountInput = listinput_and_amount[1]

Moul's avatar
Moul committed
307
    head_block = await HeadBlock().head_block
308
    currency_name = head_block["currency"]
309
    blockstamp_current = BlockUID(head_block["number"], head_block["hash"])
310
    curentUnitBase = head_block["unitbase"]
Tortue95's avatar
Tortue95 committed
311

Moul's avatar
Moul committed
312
    if not OutputbackChange:
Tortue95's avatar
Tortue95 committed
313 314
        OutputbackChange = issuers

Moul's avatar
Moul committed
315
    # if it's not a foreign exchange transaction, we remove units after 2 digits after the decimal point.
316
    if issuers not in outputAddresses:
Moul's avatar
Moul committed
317 318 319
        totalAmountTransfered = (
            totalAmountTransfered // 10 ** curentUnitBase
        ) * 10 ** curentUnitBase
Tortue95's avatar
Tortue95 committed
320

Moul's avatar
Moul committed
321
    # Generate output
Tortue95's avatar
Tortue95 committed
322 323
    ################
    listoutput = []
Moul's avatar
Moul committed
324
    # Outputs to receiver (if not himself)
325
    if isinstance(outputAddresses, str):
326
        generate_output(listoutput, curentUnitBase, AmountTransfered, outputAddresses)
327 328 329
    else:
        for outputAddress in outputAddresses:
            generate_output(listoutput, curentUnitBase, AmountTransfered, outputAddress)
Tortue95's avatar
Tortue95 committed
330

Moul's avatar
Moul committed
331
    # Outputs to himself
332
    rest = totalAmountInput - totalAmountTransfered
333
    generate_output(listoutput, curentUnitBase, rest, OutputbackChange)
Tortue95's avatar
Tortue95 committed
334

335 336 337
    # Unlocks
    unlocks = generate_unlocks(listinput)

Moul's avatar
Moul committed
338
    # Generate transaction document
Tortue95's avatar
Tortue95 committed
339 340
    ##############################

341 342 343 344 345 346 347 348 349 350 351 352 353
    return Transaction(
        version=10,
        currency=currency_name,
        blockstamp=blockstamp_current,
        locktime=0,
        issuers=[issuers],
        inputs=listinput,
        unlocks=unlocks,
        outputs=listoutput,
        comment=Comment,
        signatures=[],
    )

Tortue95's avatar
Tortue95 committed
354

355 356 357 358 359
def generate_unlocks(listinput):
    unlocks = list()
    for i in range(0, len(listinput)):
        unlocks.append(Unlock(index=i, parameters=[SIGParameter(0)]))
    return unlocks
Tortue95's avatar
Tortue95 committed
360 361


362 363 364 365 366 367 368
def generate_output(listoutput, unitbase, rest, recipient_address):
    while rest > 0:
        outputAmount = truncBase(rest, unitbase)
        rest -= outputAmount
        if outputAmount > 0:
            outputAmount = int(outputAmount / math.pow(10, unitbase))
            listoutput.append(
369 370 371 372 373
                OutputSource(
                    amount=str(outputAmount),
                    base=unitbase,
                    condition="SIG({0})".format(recipient_address),
                )
374 375 376 377
            )
        unitbase = unitbase - 1


Tortue95's avatar
Tortue95 committed
378 379
def checkComment(Comment):
    if len(Comment) > 255:
380
        message_exit("Error: Comment is too long")
Moul's avatar
Moul committed
381 382 383
    regex = compile(
        "^[0-9a-zA-Z\ \-\_\:\/\;\*\[\]\(\)\?\!\^\+\=\@\&\~\#\{\}\|\\\<\>\%\.]*$"
    )
384
    if not search(regex, Comment):
385
        message_exit("Error: the format of the comment is invalid")
Tortue95's avatar
Tortue95 committed
386

Moul's avatar
Moul committed
387

Tortue95's avatar
Tortue95 committed
388 389 390 391 392
def truncBase(amount, base):
    pow = math.pow(10, base)
    if amount < pow:
        return 0
    return math.trunc(amount / pow) * pow