def amount_check(tx, payfee_coin_id): # Inputs input_coins = Balance() for txhash, txindex in tx.inputs: input_tx = tx_builder.get_tx(txhash) if input_tx is None: raise BlockChainError('Not found input tx {}'.format(txhash.hex())) address, coin_id, amount = input_tx.outputs[txindex] input_coins[coin_id] += amount # Outputs output_coins = Balance() for address, coin_id, amount in tx.outputs: if amount <= 0: raise BlockChainError('Input amount is more than 0') output_coins[coin_id] += amount # Fee fee_coins = Balance(coin_id=payfee_coin_id, amount=tx.gas_price * tx.gas_amount) # Check all plus amount remain_amount = input_coins - output_coins - fee_coins if not remain_amount.is_empty(): raise BlockChainError( '77 Don\'t match input/output. {}={}-{}-{}'.format( remain_amount, input_coins, output_coins, fee_coins))
async def sendtoaddress(*args, **kwargs): """ Send an amount to a given address. Arguments: 1. "address" (string, required) The bitcoin address to send to. 2. "amount" (numeric or string, required) The amount in BTC to send. eg 0.1 3. "comment" (string, optional) A comment used to store what the transaction is for. This is not part of the transaction, just kept in your wallet. 4. "comment_to" (string, optional) A comment to store the name of the person or organization to which you're sending the transaction. This is not part of the transaction, just kept in your wallet. 5. subtractfeefromamount (boolean, optional, default=false) The fee will be deducted from the amount being sent. The recipient will receive less bitcoins than you enter in the amount field. Result: "txid" (string) The transaction id. """ if len(args) < 2: raise ValueError('too few arguments num={}'.format(len(args))) address, amount, *options = args if not is_address(address, V.BECH32_HRP, 0): raise ValueError('address is invalid') amount = int(amount * pow(10, V.COIN_DIGIT)) _comment = str( options[0]) if 0 < len(options) else None # do not use by Yiimp _comment_to = str( options[1]) if 1 < len(options) else None # do not use by Yiimp subtract_fee_amount = bool(options[2]) if 2 < len(options) else False # execute send error = None from_id = C.ANT_UNKNOWN coin_id = 0 coins = Balance(coin_id, amount) with create_db(V.DB_ACCOUNT_PATH) as db: cur = db.cursor() try: new_tx = send_from(from_id, address, coins, cur, subtract_fee_amount=subtract_fee_amount) if send_newtx(new_tx=new_tx, outer_cur=cur): db.commit() else: error = 'Failed to send new tx' db.rollback() except Exception as e: error = str(e) log.debug("sendtoaddress", exc_info=True) db.rollback() # submit result if error: raise ValueError(error) return new_tx.hash.hex()
def input_output_digest(tx): require_cks = set() coins = Balance() # inputs for txhash, txindex in tx.inputs: input_tx = tx_builder.get_tx(txhash=txhash) if input_tx is None: raise BlockChainError('input tx is None. {}:{}'.format(hexlify(txhash).decode(), txindex)) address, coin_id, amount = input_tx.outputs[txindex] require_cks.add(address) coins[coin_id] += amount # outputs for address, coin_id, amount in tx.outputs: coins[coin_id] -= amount # fee coins[0] -= tx.gas_amount * tx.gas_price return require_cks, coins
async def send_from_user(request): start = time() if P.F_NOW_BOOTING: return web.Response(text='Now booting', status=403) post = await utils.content_type_json_check(request) with create_db(V.DB_ACCOUNT_PATH, f_strict=True) as db: cur = db.cursor() try: from_name = post.get('from', C.account2name[C.ANT_UNKNOWN]) from_id = read_name2userid(from_name, cur) to_address = post['address'] coin_id = int(post.get('coin_id', 0)) amount = int(post['amount']) coins = Balance(coin_id, amount) if 'hex' in post: msg_type = C.MSG_BYTE msg_body = a2b_hex(post['hex']) elif 'message' in post: msg_type = post.get('message_type', C.MSG_PLAIN) msg_body = type2message(msg_type, post['message']) else: msg_type = C.MSG_NONE msg_body = b'' new_tx = send_from(from_id, to_address, coins, cur, msg_type=msg_type, msg_body=msg_body) if 'R' in post: new_tx.R = a2b_hex(post['R']) if not send_newtx(new_tx=new_tx, outer_cur=cur): raise BlockChainError('Failed to send new tx') db.commit() return utils.json_res({ 'hash': new_tx.hash.hex(), 'gas_amount': new_tx.gas_amount, 'gas_price': new_tx.gas_price, 'fee': new_tx.gas_amount * new_tx.gas_price, 'time': round(time() - start, 3) }) except Exception as e: db.rollback() return utils.error_res()
async def move_many(request): try: post = await utils.content_type_json_check(request) ant_from = post.get('from', C.account2name[C.ANT_UNKNOWN]) ant_to = post['to'] coins = Balance() for k, v in post['coins'].items(): coins[int(k)] += int(v) with create_db(V.DB_ACCOUNT_PATH, f_strict=True) as db: cur = db.cursor() _from = read_name2userid(ant_from, cur) _to = read_name2userid(ant_to, cur) txhash = user_account.move_balance(_from, _to, coins, cur) db.commit() return utils.json_res({ 'txhash': txhash.hex(), 'from_id': _from, 'to_id': _to }) except Exception as e: return web.Response(text=str(e), status=400)
async def move_one(request): try: post = await utils.content_type_json_check(request) ant_from = post.get('from', C.account2name[C.ANT_UNKNOWN]) ant_to = post['to'] coin_id = int(post.get('coin_id', 0)) amount = int(post['amount']) coins = Balance(coin_id, amount) with create_db(V.DB_ACCOUNT_PATH, f_strict=True) as db: cur = db.cursor() _from = read_name2userid(ant_from, cur) _to = read_name2userid(ant_to, cur) txhash = user_account.move_balance(_from, _to, coins, cur) db.commit() return utils.json_res({ 'txhash': txhash.hex(), 'from_id': _from, 'to_id': _to }) except Exception: return utils.error_res()
async def move_many(request): try: post = await web_base.content_type_json_check(request) ant_from = post.get('from', C.ANT_NAME_UNKNOWN) ant_to = post['to'] coins = Balance() for k, v in post['coins'].items(): coins[int(k)] += int(v) with closing(create_db(V.DB_ACCOUNT_PATH)) as db: cur = db.cursor() _from = read_name2user(ant_from, cur) _to = read_name2user(ant_to, cur) txhash = user_account.move_balance(_from, _to, coins, cur) db.commit() return web_base.json_res({ 'txhash': hexlify(txhash).decode(), 'from_id': _from, 'to_id': _to }) except Exception as e: return web.Response(text=str(e), status=400)
async def send_from_user(request): start = time() if P.F_NOW_BOOTING: return web.Response(text='Now booting...', status=403) post = await web_base.content_type_json_check(request) with closing(create_db(V.DB_ACCOUNT_PATH)) as db: cur = db.cursor() try: from_name = post.get('from', C.ANT_NAME_UNKNOWN) from_id = read_name2user(from_name, cur) to_address = post['address'] coin_id = int(post.get('coin_id', 0)) amount = int(post['amount']) coins = Balance(coin_id, amount) message = post.get('message', None) if message: msg_type = C.MSG_PLAIN msg_body = message.encode() else: msg_type = C.MSG_NONE msg_body = b'' new_tx = send_from(from_id, to_address, coins, cur, msg_type=msg_type, msg_body=msg_body) if not send_newtx(new_tx=new_tx, outer_cur=cur): raise BlockChainError('Failed to send new tx.') db.commit() return web_base.json_res({ 'hash': hexlify(new_tx.hash).decode(), 'gas_amount': new_tx.gas_amount, 'gas_price': new_tx.gas_price, 'fee': new_tx.gas_amount * new_tx.gas_price, 'time': round(time() - start, 3) }) except Exception as e: db.rollback() return web_base.error_res()
def fill_inputs_outputs(tx, cur, fee_coin_id=0, additional_gas=0, dust_percent=0.8, utxo_cashe=None): assert tx.gas_price > 0, "Gas params is none zero." # outputsの合計を取得 output_coins = Balance() for address, coin_id, amount in tx.outputs.copy(): if address == DUMMY_REDEEM_ADDRESS: # 償還Outputは再構築するので消す tx.outputs.remove((address, coin_id, amount)) continue output_coins[coin_id] += amount # 一時的にfeeの概算 fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) # 必要なだけinputsを取得 tx.inputs.clear() need_coins = output_coins + fee_coins input_coins = Balance() input_address = set() f_dust_skipped = False if utxo_cashe is None: utxo_iter = get_unspents_iter(cur) utxo_cashe = list() f_put_cashe = True else: utxo_iter = utxo_cashe f_put_cashe = False for address, height, txhash, txindex, coin_id, amount in utxo_iter: if f_put_cashe: utxo_cashe.append( (address, height, txhash, txindex, coin_id, amount)) if coin_id not in need_coins: continue elif need_coins[coin_id] * dust_percent > amount: f_dust_skipped = True continue need_coins[coin_id] -= amount input_coins[coin_id] += amount input_address.add(address) tx.inputs.append((txhash, txindex)) if need_coins.is_all_minus_amount(): break else: if f_dust_skipped and dust_percent > 0.00001: new_dust_percent = round(dust_percent * 0.7, 6) logging.debug("Retry by lower dust percent. {}=>{}".format( dust_percent, new_dust_percent)) return fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id, additional_gas=additional_gas, dust_percent=new_dust_percent, utxo_cashe=utxo_cashe) elif len(tx.inputs) > 255: raise BlockChainError( 'Too many inputs, unspent tx\'s amount is too small.') else: raise BlockChainError( 'Insufficient balance. inputs={} needs={}'.format( input_coins, need_coins)) # redeemを計算 redeem_coins = input_coins - output_coins - fee_coins for coin_id, amount in redeem_coins: tx.outputs.append((DUMMY_REDEEM_ADDRESS, coin_id, amount)) # Feeをチェックし再計算するか決める tx.serialize() need_gas_amount = tx.size + len( input_address) * C.SIGNATURE_GAS + additional_gas if 0 <= tx.gas_amount - need_gas_amount < 10000: # input/outputを混ぜる return input_address else: # insufficient gas logging.debug("Retry calculate tx fee. [{}=>{}+{}={}]".format( tx.gas_amount, tx.size + len(input_address) * C.SIGNATURE_GAS, additional_gas, need_gas_amount)) tx.gas_amount = need_gas_amount return fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id, additional_gas=additional_gas, dust_percent=dust_percent, utxo_cashe=utxo_cashe)
def send_many(sender, send_pairs, cur, fee_coin_id=0, gas_price=None, msg_type=C.MSG_NONE, msg_body=b'', subtract_fee_from_amount=False, retention=10800): assert isinstance(sender, int), 'Sender is user id' assert 0 < len(send_pairs), 'Empty send_pairs' # send_pairs check movements = Accounting() send_coins = Balance() outputs = list() coins = Balance() for address, coin_id, amount in send_pairs: assert isinstance(address, str) assert isinstance(coin_id, int) and isinstance(amount, int), 'CoinID, amount is int' coins[coin_id] += amount outputs.append((address, coin_id, amount)) user = read_address2userid(address=address, cur=cur) if user is not None: movements[user][coin_id] += amount # send to myself movements[sender] -= coins # movements[C.ANT_OUTSIDE] += coins # tx now = int(time() - V.BLOCK_GENESIS_TIME) tx = TX.from_dict( tx={ 'version': __chain_version__, 'type': C.TX_TRANSFER, 'time': now, 'deadline': now + retention, 'inputs': list(), 'outputs': outputs, 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 'gas_amount': 1, 'message_type': msg_type, 'message': msg_body }) tx.gas_amount = tx.size + C.SIGNATURE_GAS # fill unspents input_address = fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id) # subtract fee from amount if subtract_fee_from_amount: if fee_coin_id != 0: raise BlockChainError('subtract_fee option require fee_coin_id=0') subtract_fee = subtract_fee_from_user_balance(tx) # fee returns to sender's balance movements[sender][0] += subtract_fee send_coins[0] -= subtract_fee fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) # check enough balance account have for address, coin_id, amount in send_pairs: send_coins[coin_id] += amount check_enough_amount(sender=sender, send_coins=send_coins, fee_coins=fee_coins, cur=cur) # replace dummy address replace_redeem_dummy_address(tx, cur) # setup signature tx.serialize() setup_signature(tx, input_address) movements[sender] -= fee_coins # movements[C.ANT_OUTSIDE] += fee_coins insert_movelog(movements, cur, tx.type, tx.time, tx.hash) return tx
def change_mintcoin(mint_id, cur, amount=None, description=None, image=None, setting=None, new_address=None, gas_price=None, sender=C.ANT_UNKNOWN, retention=10800): assert amount or description or image or setting or new_address params = dict() if description: params['description'] = description if image: params['image'] = image if new_address: params['address'] = new_address if len(params) == 0: params = None if not params and not setting and not amount: raise BlockChainError('No update found.') m_before = get_mintcoin_object(coin_id=mint_id) if m_before.version == -1: raise BlockChainError('Not init mintcoin. {}'.format(m_before)) result = check_mintcoin_new_format(m_before=m_before, new_params=params, new_setting=setting) if isinstance(result, str): raise BlockChainError('check_mintcoin_new_format(): {}'.format(result)) msg_body = bjson.dumps((mint_id, params, setting), compress=False) tx = TX( tx={ 'type': C.TX_MINT_COIN, 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 'gas_amount': 1, 'message_type': C.MSG_BYTE, 'message': msg_body }) if amount: tx.outputs.append((MINTCOIN_DUMMY_ADDRESS, 0, amount)) send_coins = Balance(0, amount) minting_coins = Balance(mint_id, amount) else: send_coins = Balance(0, 0) minting_coins = Balance(0, 0) tx.update_time(retention) additional_gas = C.MINTCOIN_GAS + C.SIGNATURE_GAS # for mint_coin user signature tx.gas_amount = tx.size + C.SIGNATURE_GAS + additional_gas tx.serialize() # fill unspents fee_coin_id = 0 input_address = fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id, additional_gas=additional_gas) input_address.add(m_before.address) fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) # check amount check_enough_amount(sender=sender, send_coins=send_coins, fee_coins=fee_coins) # replace dummy address replace_redeem_dummy_address(tx=tx, cur=cur) # replace dummy mint_id replace_mint_dummy_address(tx=tx, mint_address=m_before.address, mint_id=mint_id, f_raise=False) # setup signature tx.serialize() setup_signature(tx=tx, input_address=input_address) # movement movements = Accounting() movements[sender] += minting_coins movements[C.ANT_OUTSIDE] -= minting_coins movements[sender] -= fee_coins movements[C.ANT_OUTSIDE] += fee_coins insert_log(movements, cur, tx.type, tx.time, tx.hash) return tx
def issue_mintcoin(name, unit, digit, amount, cur, description=None, image=None, additional_issue=True, change_address=True, gas_price=None, sender=C.ANT_UNKNOWN, retention=10800): mint_id = get_new_coin_id() sender_name = read_user2name(user=sender, cur=cur) mint_address = create_new_user_keypair(name=sender_name, cur=cur) params = { "name": name, "unit": unit, "digit": digit, "address": mint_address, "description": description, "image": image } setting = { "additional_issue": additional_issue, "change_address": change_address } m_before = get_mintcoin_object(coin_id=mint_id) result = check_mintcoin_new_format(m_before=m_before, new_params=params, new_setting=setting) if isinstance(result, str): raise BlockChainError('check_mintcoin_new_format(): {}'.format(result)) msg_body = bjson.dumps((mint_id, params, setting), compress=False) tx = TX( tx={ 'type': C.TX_MINT_COIN, 'inputs': list(), 'outputs': [(MINTCOIN_DUMMY_ADDRESS, 0, amount)], 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 'gas_amount': 1, 'message_type': C.MSG_BYTE, 'message': msg_body }) tx.update_time(retention) additional_gas = C.MINTCOIN_GAS tx.gas_amount = tx.size + C.SIGNATURE_GAS + additional_gas tx.serialize() # fill unspents fee_coin_id = 0 input_address = fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id, additional_gas=additional_gas) # input_address.add(mint_address) fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) # check amount check_enough_amount(sender=sender, send_coins=Balance(0, amount), fee_coins=fee_coins) # replace dummy address replace_redeem_dummy_address(tx=tx, cur=cur) # replace dummy mint_id replace_mint_dummy_address(tx=tx, mint_address=mint_address, mint_id=mint_id, f_raise=True) # setup signature tx.serialize() setup_signature(tx=tx, input_address=input_address) # movement movements = Accounting() minting_coins = Balance(mint_id, amount) movements[sender] += minting_coins movements[C.ANT_OUTSIDE] -= minting_coins movements[sender] -= fee_coins movements[C.ANT_OUTSIDE] += fee_coins insert_log(movements, cur, tx.type, tx.time, tx.hash) return mint_id, tx
def fill_inputs_outputs(tx, target_address=None, cur=None, signature_num=None, fee_coin_id=0, additional_gas=0, dust_percent=0.8, utxo_cashe=None, depth=0): if MAX_RECURSIVE_DEPTH < depth: raise BlockChainError('over max recursive depth on filling inputs_outputs!') # outputsの合計を取得 output_coins = Balance() for address, coin_id, amount in tx.outputs.copy(): if address == DUMMY_REDEEM_ADDRESS: # 償還Outputは再構築するので消す tx.outputs.remove((address, coin_id, amount)) continue output_coins[coin_id] += amount # 一時的にfeeの概算 fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) # 必要なだけinputsを取得 tx.inputs.clear() need_coins = output_coins + fee_coins input_coins = Balance() input_address = set() f_dust_skipped = False if utxo_cashe is None: if target_address: utxo_iter = get_unspents_iter(target_address=target_address) elif cur: utxo_iter = get_my_unspents_iter(cur=cur) else: raise Exception('target_address and cur is None?') cashe = list() utxo_cashe = [cashe, utxo_iter] else: cashe, utxo_iter = utxo_cashe for is_cashe, (address, height, txhash, txindex, coin_id, amount) in sum_utxo_iter(cashe, utxo_iter): if not is_cashe: cashe.append((address, height, txhash, txindex, coin_id, amount)) if coin_id not in need_coins: continue if need_coins[coin_id] * dust_percent > amount: f_dust_skipped = True continue need_coins[coin_id] -= amount input_coins[coin_id] += amount input_address.add(address) tx.inputs.append((txhash, txindex)) if need_coins.is_all_minus_amount(): break else: if f_dust_skipped and dust_percent > 0.00001: new_dust_percent = round(dust_percent * 0.7, 6) log.debug("Retry by lower dust percent. {}=>{}".format(dust_percent, new_dust_percent)) return fill_inputs_outputs( tx=tx, target_address=target_address, cur=cur, signature_num=signature_num, fee_coin_id=fee_coin_id, additional_gas=additional_gas, dust_percent=new_dust_percent, utxo_cashe=utxo_cashe, depth=depth+1) elif len(tx.inputs) > 255: raise BlockChainError('TX inputs is too many num={}'.format(len(tx.inputs))) else: raise BlockChainError('Insufficient balance. inputs={} needs={}'.format(input_coins, need_coins)) # redeemを計算 redeem_coins = input_coins - output_coins - fee_coins for coin_id, amount in redeem_coins: tx.outputs.append((DUMMY_REDEEM_ADDRESS, coin_id, amount)) # Feeをチェックし再計算するか決める tx.serialize() if signature_num is None: need_gas_amount = tx.size + additional_gas + len(input_address) * C.SIGNATURE_GAS else: need_gas_amount = tx.size + additional_gas + signature_num * C.SIGNATURE_GAS if tx.gas_amount > need_gas_amount: # swap overflowed gas, gas_amount => redeem_output swap_amount = (tx.gas_amount - need_gas_amount) * tx.gas_price for index, (address, coin_id, amount) in enumerate(tx.outputs): if address != DUMMY_REDEEM_ADDRESS: continue elif coin_id != fee_coin_id: continue else: tx.outputs[index] = (address, coin_id, amount + swap_amount) break else: raise BlockChainError('cannot swap overflowed gas amount={}'.format(swap_amount)) # success swap tx.gas_amount = need_gas_amount tx.serialize() return input_address elif tx.gas_amount < need_gas_amount: # retry insufficient gas log.info("retry calculate fee gasBefore={} gasNext={}".format(tx.gas_amount, need_gas_amount)) tx.gas_amount = need_gas_amount return fill_inputs_outputs( tx=tx, target_address=target_address, cur=cur, signature_num=signature_num, fee_coin_id=fee_coin_id, additional_gas=additional_gas, dust_percent=dust_percent, utxo_cashe=utxo_cashe, depth=depth+1) else: # tx.gas_amount == need_gas_amount return input_address
def send_many(sender, send_pairs, cur, fee_coin_id=0, gas_price=None, msg_type=C.MSG_NONE, msg_body=b'', f_balance_check=True, retention=10800): assert isinstance(sender, int), 'Sender is user id.' assert 0 < len(send_pairs), 'Empty send_pairs.' # send_pairs check movements = Accounting() outputs = list() coins = Balance() for address, coin_id, amount in send_pairs: assert isinstance( address, str) and len(address) == 40, 'Recipient is 40 letter string.' assert isinstance(coin_id, int) and isinstance( amount, int), 'CoinID, amount is int.' coins[coin_id] += amount outputs.append((address, coin_id, amount)) movements[sender] -= coins movements[C.ANT_OUTSIDE] += coins # tx now = int(time.time() - V.BLOCK_GENESIS_TIME) tx = TX( tx={ 'version': __chain_version__, 'type': C.TX_TRANSFER, 'time': now, 'deadline': now + retention, 'inputs': list(), 'outputs': outputs, 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 'gas_amount': 1, 'message_type': msg_type, 'message': msg_body }) tx.gas_amount = tx.size + C.SIGNATURE_GAS # fill unspents input_address = fill_inputs_outputs(tx, cur, fee_coin_id, additional_gas=0) # account check fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) if f_balance_check: # 残高が十分にあるかチェック send_coins = Balance() for address, coin_id, amount in send_pairs: send_coins[coin_id] += amount check_enough_amount(sender, send_coins, fee_coins) if sender in (C.ANT_OUTSIDE, C.ANT_RESERVED): # 内部アカウントは不可 raise BlockChainError('Not allowed inner account.') # replace dummy address replace_redeem_dummy_address(tx, cur) # setup signature tx.serialize() setup_signature(tx, input_address) movements[sender] -= fee_coins movements[C.ANT_OUTSIDE] += fee_coins insert_log(movements, cur, tx.type, tx.time, tx.hash) return tx