def build_transaction(self, wallet, htlc, amount, locktime=0): """ Build bitcoin fund transaction. :param wallet: bitcoin sender wallet. :type wallet: bitcoin.wallet.Wallet :param htlc: bitcoin hash time lock contract (HTLC). :type htlc: bitcoin.htlc.HTLC :param amount: bitcoin amount to fund. :type amount: int :param locktime: bitcoin transaction lock time, defaults to 0. :type locktime: int :returns: FundTransaction -- bitcoin fund transaction instance. >>> from shuttle.providers.bitcoin.transaction import FundTransaction >>> fund_transaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(sender_wallet, htlc, 10000) <shuttle.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Checking build transaction arguments instance if not isinstance(wallet, Wallet): raise TypeError( "invalid wallet instance, only takes bitcoin Wallet class") if not isinstance(htlc, HTLC): raise TypeError( "invalid htlc instance, only takes bitcoin HTLC class") if not isinstance(amount, int): raise TypeError("invalid amount instance, only takes integer type") # Setting wallet, htlc, amount and unspent self.wallet, self.htlc, self.amount = wallet, htlc, amount # Getting unspent transaction output self.unspent = self.wallet.unspent() # Setting previous transaction indexes self.previous_transaction_indexes = \ self.get_previous_transaction_indexes(amount=self.amount) # Getting transaction inputs and amount inputs, amount = self.inputs(self.unspent, self.previous_transaction_indexes) # Calculating bitcoin fee self.fee = fee_calculator(len(inputs), 2) if amount < (self.amount + self.fee): raise BalanceError("insufficient spend utxos") # Building mutable bitcoin transaction self.transaction = MutableTransaction( version=self.version, ins=inputs, outs=[ # Funding into hash time lock contract script hash TxOut(value=self.amount, n=0, script_pubkey=P2shScript.unhexlify(self.htlc.hash())), # Controlling amounts when we are funding on htlc script. TxOut(value=amount - (self.fee + self.amount), n=1, script_pubkey=P2pkhScript.unhexlify(self.wallet.p2pkh())) ], locktime=Locktime(locktime)) return self
def open_tx(committer, secret, commit_tx_hash): # 创建输入脚本 p2pkh_solver = P2pkhSolver(committer.privk) hasklock_solver = HashlockSolver(secret.encode(), p2pkh_solver) if_solver = IfElseSolver( Branch.IF, # branch selection hasklock_solver) # 创建输出脚本 script = P2pkhScript(committer.pubk) # 获取commit交易 to_spend_raw = get_raw_tx(commit_tx_hash, coin_symbol) to_spend = TransactionFactory.unhexlify(to_spend_raw) # 获取罚金数额 penalty = int(float(to_spend.to_json()['vout'][0]['value']) * (10**8)) # 估算挖矿费用 print('estimating mining fee...') mining_fee_per_kb = get_mining_fee_per_kb(coin_symbol, committer.api_key, condidence='high') estimated_tx_size = cal_tx_size_in_byte(inputs_num=1, outputs_num=1) mining_fee = int(mining_fee_per_kb * (estimated_tx_size / 1000)) * 2 # 创建交易 unsigned = MutableTransaction(version=2, ins=[ TxIn(txid=to_spend.txid, txout=0, script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=[ TxOut(value=penalty - mining_fee, n=0, script_pubkey=script), ], locktime=Locktime(0)) # 修改交易 signed = unsigned.spend([to_spend.outs[0]], [if_solver]) # 广播交易 print('open_tx_hex: ', signed.hexlify()) msg = pushtx(coin_symbol=coin_symbol, api_key=committer.api_key, tx_hex=signed.hexlify()) format_output(msg) return msg['tx']['hash']
def decode_transaction_raw(transaction_raw): """ Decode Bitcoin transaction raw. :param transaction_raw: Bitcoin transaction raw. :type transaction_raw: str :returns: dict -- decoded Bitcoin transaction. >>> from swap.providers.bitcoin.utils import decode_transaction_raw >>> transaction_raw = "eyJmZWUiOiA2NzgsICJyYXciOiAiMDIwMDAwMDAwMTg4OGJlN2VjMDY1MDk3ZDk1NjY0NzYzZjI3NmQ0MjU1NTJkNzM1ZmIxZDk3NGFlNzhiZjcyMTA2ZGNhMGYzOTEwMTAwMDAwMDAwZmZmZmZmZmYwMjEwMjcwMDAwMDAwMDAwMDAxN2E5MTQyYmIwMTNjM2U0YmViMDg0MjFkZWRjZjgxNWNiNjVhNWMzODgxNzhiODdiY2RkMGUwMDAwMDAwMDAwMTk3NmE5MTQ2NGE4MzkwYjBiMTY4NWZjYmYyZDRiNDU3MTE4ZGM4ZGE5MmQ1NTM0ODhhYzAwMDAwMDAwIiwgIm91dHB1dHMiOiBbeyJhbW91bnQiOiA5ODQ5NDYsICJuIjogMSwgInNjcmlwdCI6ICI3NmE5MTQ2NGE4MzkwYjBiMTY4NWZjYmYyZDRiNDU3MTE4ZGM4ZGE5MmQ1NTM0ODhhYyJ9XSwgIm5ldHdvcmsiOiAidGVzdG5ldCIsICJ0eXBlIjogImJpdGNvaW5fZnVuZF91bnNpZ25lZCJ9" >>> decode_transaction_raw(transaction_raw) {'fee': 678, 'type': 'bitcoin_fund_unsigned', 'tx': {'hex': '0200000001888be7ec065097d95664763f276d425552d735fb1d974ae78bf72106dca0f3910100000000ffffffff02102700000000000017a9142bb013c3e4beb08421dedcf815cb65a5c388178b87bcdd0e00000000001976a91464a8390b0b1685fcbf2d4b457118dc8da92d553488ac00000000', 'txid': 'abc70fd3466aec9478ea3115200a84f993204ad1f614fe08e92ecc5997a0d3ba', 'hash': 'abc70fd3466aec9478ea3115200a84f993204ad1f614fe08e92ecc5997a0d3ba', 'size': 117, 'vsize': 117, 'version': 2, 'locktime': 0, 'vin': [{'txid': '91f3a0dc0621f78be74a971dfb35d75255426d273f766456d9975006ece78b88', 'vout': 1, 'scriptSig': {'asm': '', 'hex': ''}, 'sequence': '4294967295'}], 'vout': [{'value': '0.00010000', 'n': 0, 'scriptPubKey': {'asm': 'OP_HASH160 2bb013c3e4beb08421dedcf815cb65a5c388178b OP_EQUAL', 'hex': 'a9142bb013c3e4beb08421dedcf815cb65a5c388178b87', 'type': 'p2sh', 'address': '2MwEDybGC34949zgzWX4M9FHmE3crDSUydP'}}, {'value': '0.00974268', 'n': 1, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 64a8390b0b1685fcbf2d4b457118dc8da92d5534 OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a91464a8390b0b1685fcbf2d4b457118dc8da92d553488ac', 'type': 'p2pkh', 'address': 'mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q'}}]}, 'network': 'testnet'} """ transaction_raw = str(transaction_raw + "=" * (-len(transaction_raw) % 4)) try: decoded_transaction_raw = json.loads( b64decode(str(transaction_raw).encode()).decode()) except (binascii.Error, json.decoder.JSONDecodeError) as _error: raise ValueError("invalid Bitcoin transaction raw") if "type" not in decoded_transaction_raw or not str( decoded_transaction_raw["type"]).startswith("bitcoin"): raise ValueError("invalid Bitcoin transaction raw") # Setting testnet stp(decoded_transaction_raw["network"], strict=True) tx = MutableTransaction.unhexlify(decoded_transaction_raw["raw"]) return dict(fee=decoded_transaction_raw["fee"], type=decoded_transaction_raw["type"], tx=tx.to_json(), network=decoded_transaction_raw["network"])
def decode_transaction_raw(transaction_raw): """ Decode Bitcoin transaction raw. :param transaction_raw: Bitcoin transaction raw. :type transaction_raw: str :returns: dict -- decoded Bitcoin transaction. >>> from shuttle.providers.bitcoin.utils import decode_transaction_raw >>> decode_transaction_raw(transaction_raw) {...} """ transaction_raw = str(transaction_raw + "=" * (-len(transaction_raw) % 4)) try: decoded_transaction_raw = json.loads(b64decode(str(transaction_raw).encode()).decode()) except (binascii.Error, json.decoder.JSONDecodeError) as _error: raise ValueError("invalid Bitcoin transaction raw") if "type" not in decoded_transaction_raw or not str(decoded_transaction_raw["type"]).startswith("bitcoin"): raise ValueError("invalid Bitcoin transaction raw") # Setting testnet stp(decoded_transaction_raw["network"], strict=True) tx = MutableTransaction.unhexlify(decoded_transaction_raw["raw"]) return dict( fee=decoded_transaction_raw["fee"], type=decoded_transaction_raw["type"], tx=tx.to_json(), network=decoded_transaction_raw["network"] )
def make_raw_transaction(inputs: list, outputs: list, locktime=Locktime(0), timestamp: int = int(time()), version=1): '''create raw transaction''' return MutableTransaction(version, timestamp, inputs, outputs, locktime)
def spendCb(fee, reward, objTx, outputs, cbSolver, dictIdToPub): """ create a single coinbase spend :param fee: fee :param reward: block reward in sat :param objTx: coinbase tx :param outputs: lst of channels (copies from originals) :param cbSolver: solver :param bPubMiner: pub of the miner :return: tx that spends coinbase """ outs = [] totVal = 0 chanIds = [] for o in outputs: v = int(o.value) bPubN1 = dictIdToPub[str(o.node1.nodeid)] bPubN2 = dictIdToPub[str(o.node2.nodeid)] if bPubN1.compressed < bPubN2.compressed: # lexicographical ordering multisig_script = MultisigScript(2, bPubN1, bPubN2, 2) else: multisig_script = MultisigScript(2, bPubN2, bPubN1, 2) p2wsh_multisig = P2wshV0Script(multisig_script) totVal += v outs += [TxOut(value=v, n=0, script_pubkey=p2wsh_multisig)] chanIds += [o.channelid] change = reward - totVal - fee outsWithChange = outs + [ TxOut(value=change, n=0, script_pubkey=objTx.outs[0].script_pubkey) ] unsignedCb = MutableTransaction(version=1, ins=[ TxIn(txid=objTx.txid, txout=0, script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=outsWithChange, locktime=Locktime(0)) cbTx = unsignedCb.spend([objTx.outs[0]], [cbSolver]) return cbTx, (cbTx.txid, chanIds)
def _get_unsigned_txn(self): # outputs_amounts is copied so any instance can be modified with change_fee, # and will still function correctly, i.e the change address won't already # be in the self._outputs_amounts dict self._modified_outputs_amounts = self.outputs_amounts.copy() # adding change address to outputs, if there is leftover balance that isn't dust if self._change_amount > 0: self._modified_outputs_amounts[self.change_address] = self._change_amount outputs = [] for i, (addr, amount) in enumerate(self._modified_outputs_amounts.items()): outputs.append(TxOut( value=amount, n=i, script_pubkey=self.get_script_pubkey(addr) )) inputs = [] for t in self._specific_utxo_data: # build inputs using the UTXO data in self._specific_utxo_data, # script_sig is empty as the transaction will be signed later inputs.append( TxIn(txid=t[0], txout=t[1], script_sig=ScriptSig.empty(), sequence=Sequence.max(), witness=Witness([StackData.zero()])) if self.is_segwit else None, ) if self.is_segwit: transaction = MutableSegWitTransaction( version=TX_VERSION, ins=inputs, outs=outputs, locktime=Locktime(self.locktime), ) else: transaction = MutableTransaction( version=TX_VERSION, ins=inputs, outs=outputs, locktime=Locktime(self.locktime) ) return transaction
def decode_transaction_raw(transaction_raw: str, offline: bool = True, headers: dict = config["headers"], timeout: int = config["timeout"]) -> dict: """ Decode Bitcoin transaction raw. :param transaction_raw: Bitcoin transaction raw. :type transaction_raw: str :param offline: Offline decode, defaults to True. :type offline: bool :param headers: Request headers, default to common headers. :type headers: dict :param timeout: Request timeout, default to 60. :type timeout: int :returns: dict -- Decoded Bitcoin transaction raw. >>> from swap.providers.bitcoin.utils import decode_transaction_raw >>> transaction_raw = "eyJmZWUiOiA2NzgsICJyYXciOiAiMDIwMDAwMDAwMTg4OGJlN2VjMDY1MDk3ZDk1NjY0NzYzZjI3NmQ0MjU1NTJkNzM1ZmIxZDk3NGFlNzhiZjcyMTA2ZGNhMGYzOTEwMTAwMDAwMDAwZmZmZmZmZmYwMjEwMjcwMDAwMDAwMDAwMDAxN2E5MTQyYmIwMTNjM2U0YmViMDg0MjFkZWRjZjgxNWNiNjVhNWMzODgxNzhiODdiY2RkMGUwMDAwMDAwMDAwMTk3NmE5MTQ2NGE4MzkwYjBiMTY4NWZjYmYyZDRiNDU3MTE4ZGM4ZGE5MmQ1NTM0ODhhYzAwMDAwMDAwIiwgIm91dHB1dHMiOiBbeyJhbW91bnQiOiA5ODQ5NDYsICJuIjogMSwgInNjcmlwdCI6ICI3NmE5MTQ2NGE4MzkwYjBiMTY4NWZjYmYyZDRiNDU3MTE4ZGM4ZGE5MmQ1NTM0ODhhYyJ9XSwgIm5ldHdvcmsiOiAidGVzdG5ldCIsICJ0eXBlIjogImJpdGNvaW5fZnVuZF91bnNpZ25lZCJ9" >>> decode_transaction_raw(transaction_raw=transaction_raw) {'fee': 678, 'type': 'bitcoin_fund_unsigned', 'tx': {'hex': '0200000001888be7ec065097d95664763f276d425552d735fb1d974ae78bf72106dca0f3910100000000ffffffff02102700000000000017a9142bb013c3e4beb08421dedcf815cb65a5c388178b87bcdd0e00000000001976a91464a8390b0b1685fcbf2d4b457118dc8da92d553488ac00000000', 'txid': 'abc70fd3466aec9478ea3115200a84f993204ad1f614fe08e92ecc5997a0d3ba', 'hash': 'abc70fd3466aec9478ea3115200a84f993204ad1f614fe08e92ecc5997a0d3ba', 'size': 117, 'vsize': 117, 'version': 2, 'locktime': 0, 'vin': [{'txid': '91f3a0dc0621f78be74a971dfb35d75255426d273f766456d9975006ece78b88', 'vout': 1, 'scriptSig': {'asm': '', 'hex': ''}, 'sequence': '4294967295'}], 'vout': [{'value': '0.00010000', 'n': 0, 'scriptPubKey': {'asm': 'OP_HASH160 2bb013c3e4beb08421dedcf815cb65a5c388178b OP_EQUAL', 'hex': 'a9142bb013c3e4beb08421dedcf815cb65a5c388178b87', 'type': 'p2sh', 'address': '2MwEDybGC34949zgzWX4M9FHmE3crDSUydP'}}, {'value': '0.00974268', 'n': 1, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 64a8390b0b1685fcbf2d4b457118dc8da92d5534 OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a91464a8390b0b1685fcbf2d4b457118dc8da92d553488ac', 'type': 'p2pkh', 'address': 'mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q'}}]}, 'network': 'testnet'} """ if not is_transaction_raw(transaction_raw=transaction_raw): raise TransactionRawError("Invalid Bitcoin transaction raw.") transaction_raw = clean_transaction_raw(transaction_raw) decoded_transaction_raw = b64decode(transaction_raw.encode()) loaded_transaction_raw = json.loads(decoded_transaction_raw.decode()) decoded_transaction: Optional[dict] = None if offline: stp(loaded_transaction_raw["network"], strict=True, force=True) tx = MutableTransaction.unhexlify(loaded_transaction_raw["raw"]) decoded_transaction = tx.to_json() else: url = f"{config[loaded_transaction_raw['network']]['blockcypher']['url']}/txs/decode" parameter = dict(token=config[loaded_transaction_raw["network"]] ["blockcypher"]["token"]) data = dict(tx=loaded_transaction_raw["raw"]) response = requests.post(url=url, data=json.dumps(data), params=parameter, headers=headers, timeout=timeout) decoded_transaction = response.json() return dict(fee=loaded_transaction_raw["fee"], type=loaded_transaction_raw["type"], tx=decoded_transaction, network=loaded_transaction_raw["network"])
def sign(self, unsigned_raw, solver): """ Sign unsigned refund transaction raw. :param unsigned_raw: bitcoin unsigned refund transaction raw. :type unsigned_raw: str :param solver: bitcoin refund solver. :type solver: bitcoin.solver.RefundSolver :returns: RefundSignature -- bitcoin refund signature instance. >>> from shuttle.providers.bitcoin.signature import RefundSignature >>> refund_signature = RefundSignature() >>> refund_signature.sign(bitcoin_refund_unsigned, refund_solver) <shuttle.providers.bitcoin.signature.RefundSignature object at 0x0409DAF0> """ tx_raw = json.loads(b64decode(str(unsigned_raw).encode()).decode()) if "raw" not in tx_raw or "outputs" not in tx_raw or "type" not in tx_raw or \ "recipient_address" not in tx_raw or "sender_address" not in tx_raw or "fee" not in tx_raw: raise ValueError("invalid unsigned refund transaction raw") self.fee = tx_raw["fee"] self.type = tx_raw["type"] if not self.type == "bitcoin_refund_unsigned": raise TypeError("can't sign this %s transaction using RefundSignature" % tx_raw["type"]) if not isinstance(solver, RefundSolver): raise Exception("invalid solver error, only refund solver") htlc = HTLC(network=self.network).init( secret_hash=sha256(solver.secret).hex(), recipient_address=tx_raw["recipient_address"], sender_address=tx_raw["sender_address"], sequence=solver.sequence ) output = TxOut(value=tx_raw["outputs"][0]["amount"], n=tx_raw["outputs"][0]["n"], script_pubkey=P2shScript.unhexlify(tx_raw["outputs"][0]["script"])) self.transaction = MutableTransaction.unhexlify(tx_raw["raw"]) self.transaction.spend([output], [ P2shSolver(htlc.script, solver.solve()) ]) self.signed = b64encode(str(json.dumps(dict( raw=self.transaction.hexlify(), fee=tx_raw["fee"], network=tx_raw["network"], type="bitcoin_refund_signed" ))).encode()).decode() return self
def decode_raw(raw: str, network: str = config["network"], offline: bool = True, headers: dict = config["headers"], timeout: int = config["timeout"]) -> dict: """ Decode original Bitcoin raw. :param raw: Bitcoin transaction raw. :type raw: str :param network: Bitcoin network, defaults to mainnet. :type network: str :param offline: Offline decode, defaults to True. :type offline: bool :param headers: Request headers, default to common headers. :type headers: dict :param timeout: Request timeout, default to 60. :type timeout: int :returns: dict -- Bitcoin decoded transaction raw. >>> from swap.providers.bitcoin.rpc import decode_raw >>> decode_raw(raw="02000000011823f39a8c5f6f27845dd13a65e03fe2ef5108d235e7a36edb6eb267b0459c5a010000006a47304402207018b7fd1ba6624fe9bb0f16cd65fa243d202e32fdff452699f56465b61ab648022009f0dc1a0a63109246c45e120fc0d34b40e789dfc4d05e64f269602c7d67d9210121027f0dc0894bd690635412af782d05e4f79d3d40bf568978c650f3f1ca1a96cf36ffffffff02102700000000000017a9149418feed4647e156d6663db3e0cef7c050d038678734330100000000001976a91433ecab3d67f0e2bde43e52f41ec1ecbdc73f11f888ac00000000", network="testnet") {'hex': '02000000011823f39a8c5f6f27845dd13a65e03fe2ef5108d235e7a36edb6eb267b0459c5a010000006a47304402207018b7fd1ba6624fe9bb0f16cd65fa243d202e32fdff452699f56465b61ab648022009f0dc1a0a63109246c45e120fc0d34b40e789dfc4d05e64f269602c7d67d9210121027f0dc0894bd690635412af782d05e4f79d3d40bf568978c650f3f1ca1a96cf36ffffffff02102700000000000017a9149418feed4647e156d6663db3e0cef7c050d038678734330100000000001976a91433ecab3d67f0e2bde43e52f41ec1ecbdc73f11f888ac00000000', 'txid': '6e5c80f600f45acda3c3101128bb3075bf2cf7af4bab0d99c9d856ebfb4b0953', 'hash': '6e5c80f600f45acda3c3101128bb3075bf2cf7af4bab0d99c9d856ebfb4b0953', 'size': 223, 'vsize': 223, 'version': 2, 'locktime': 0, 'vin': [{'txid': '5a9c45b067b26edb6ea3e735d20851efe23fe0653ad15d84276f5f8c9af32318', 'vout': 1, 'scriptSig': {'asm': '304402207018b7fd1ba6624fe9bb0f16cd65fa243d202e32fdff452699f56465b61ab648022009f0dc1a0a63109246c45e120fc0d34b40e789dfc4d05e64f269602c7d67d92101 027f0dc0894bd690635412af782d05e4f79d3d40bf568978c650f3f1ca1a96cf36', 'hex': '47304402207018b7fd1ba6624fe9bb0f16cd65fa243d202e32fdff452699f56465b61ab648022009f0dc1a0a63109246c45e120fc0d34b40e789dfc4d05e64f269602c7d67d9210121027f0dc0894bd690635412af782d05e4f79d3d40bf568978c650f3f1ca1a96cf36'}, 'sequence': '4294967295'}], 'vout': [{'value': '0.00010000', 'n': 0, 'scriptPubKey': {'asm': 'OP_HASH160 9418feed4647e156d6663db3e0cef7c050d03867 OP_EQUAL', 'hex': 'a9149418feed4647e156d6663db3e0cef7c050d0386787', 'type': 'p2sh', 'address': '2N6kHwQy6Ph5EdKNgzGrcW2WhGHKGfmP5ae'}}, {'value': '0.00078644', 'n': 1, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 33ecab3d67f0e2bde43e52f41ec1ecbdc73f11f8 OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a91433ecab3d67f0e2bde43e52f41ec1ecbdc73f11f888ac', 'type': 'p2pkh', 'address': 'mkFWGt4hT11XS8dJKzzRFsTrqjjAwZfQAC'}}]} """ if not is_network(network=network): raise NetworkError(f"Invalid Bitcoin '{network}' network", "choose only 'mainnet' or 'testnet' networks.") if offline: stp(network, strict=True, force=True) tx = MutableTransaction.unhexlify(raw) return tx.to_json() url = f"{config[network]['blockcypher']['url']}/txs/decode" parameter = dict(token=config[network]["blockcypher"]["token"]) data = dict(tx=raw) response = requests.post( url=url, data=json.dumps(data), params=parameter, headers=headers, timeout=timeout ) response_json = response.json() return response_json
def sign(self, unsigned_raw, solver): """ Sign unsigned fund transaction raw. :param unsigned_raw: bitcoin unsigned fund transaction raw. :type unsigned_raw: str :param solver: bitcoin fund solver. :type solver: bitcoin.solver.FundSolver :returns: FundSignature -- bitcoin fund signature instance. >>> from shuttle.providers.bitcoin.signature import FundSignature >>> fund_signature = FundSignature() >>> fund_signature.sign(bitcoin_fund_unsigned, fund_solver) <shuttle.providers.bitcoin.signature.FundSignature object at 0x0409DAF0> """ tx_raw = json.loads(b64decode(str(unsigned_raw).encode()).decode()) if "raw" not in tx_raw or "outputs" not in tx_raw or "type" not in tx_raw or "fee" not in tx_raw: raise ValueError("invalid unsigned fund transaction raw") self.fee = tx_raw["fee"] self.type = tx_raw["type"] if not self.type == "bitcoin_fund_unsigned": raise TypeError("can't sign this %s transaction using FundSignature" % tx_raw["type"]) if not isinstance(solver, FundSolver): raise TypeError("invalid solver instance, only takes bitcoin FundSolver class") self.transaction = MutableTransaction.unhexlify(tx_raw["raw"]) outputs = list() for output in tx_raw["outputs"]: outputs.append( TxOut(value=output["amount"], n=output["n"], script_pubkey=Script.unhexlify(output["script"]))) self.transaction.spend(outputs, [solver.solve() for _ in outputs]) self.signed = b64encode(str(json.dumps(dict( raw=self.transaction.hexlify(), fee=tx_raw["fee"], network=tx_raw["network"], type="bitcoin_fund_signed" ))).encode()).decode() return self
def sign(self, transaction_raw: str, solver: WithdrawSolver) -> "WithdrawSignature": """ Sign unsigned withdraw transaction raw. :param transaction_raw: Bitcoin unsigned withdraw transaction raw. :type transaction_raw: str :param solver: Bitcoin withdraw solver. :type solver: bitcoin.solver.WithdrawSolver :returns: WithdrawSignature -- Bitcoin withdraw signature instance. >>> from swap.providers.bitcoin.signature import WithdrawSignature >>> from swap.providers.bitcoin.solver import WithdrawSolver >>> unsigned_withdraw_transaction_raw: str = "eyJmZWUiOiA1NzYsICJyYXciOiAiMDIwMDAwMDAwMTMxZmI3NmEwYzM4ZDU3MzgxYjMxMTBlNGY1ZWU5YjUyODFkY2YyZmJlMmZlMjU2OTI2NmI3NTEwMTFkMjExYTIwMDAwMDAwMDAwZmZmZmZmZmYwMTYwODQwMTAwMDAwMDAwMDAxOTc2YTkxNDBhMGE2NTkwZTZiYTRiNDgxMThkMjFiODY4MTI2MTUyMTllY2U3NmI4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IHsidmFsdWUiOiAxMDAwMDAsICJ0eF9vdXRwdXRfbiI6IDAsICJzY3JpcHQiOiAiYTkxNGM4Yzc3YTliNDNlZTJiZGYxYTA3YzQ4Njk5ODMzZDc2NjhiZjI2NGM4NyJ9LCAibmV0d29yayI6ICJ0ZXN0bmV0IiwgInR5cGUiOiAiYml0Y29pbl93aXRoZHJhd191bnNpZ25lZCJ9" >>> bytecode: str = "63aa20821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e01588876a9140a0a6590e6ba4b48118d21b86812615219ece76b88ac67040ec4d660b17576a914e00ff2a640b7ce2d336860739169487a57f84b1588ac68" >>> withdraw_solver: WithdrawSolver = WithdrawSolver(xprivate_key="tprv8ZgxMBicQKsPf949JcuVFLXPJ5m4VKe33gVX3FYVZYVHr2dChU8K66aEQcPdHpUgACq5GQu81Z4e3QN1vxCrV4pxcUcXHoRTamXBRaPdJhW", secret_key="Hello Meheret!", bytecode=bytecode) >>> withdraw_signature: WithdrawSignature = WithdrawSignature(network="testnet") >>> withdraw_signature.sign(transaction_raw=unsigned_withdraw_transaction_raw, solver=withdraw_solver) <swap.providers.bitcoin.signature.WithdrawSignature object at 0x0409DAF0> """ if not is_transaction_raw(transaction_raw=transaction_raw): raise TransactionRawError("Invalid Bitcoin unsigned transaction raw.") transaction_raw = clean_transaction_raw(transaction_raw) decoded_transaction_raw = b64decode(transaction_raw.encode()) loaded_transaction_raw = json.loads(decoded_transaction_raw.decode()) if not loaded_transaction_raw["type"] == "bitcoin_withdraw_unsigned": raise TypeError(f"Invalid Bitcoin withdraw unsigned transaction raw type, " f"you can't sign {loaded_transaction_raw['type']} type by using withdraw signature.") # Check parameter instances if not isinstance(solver, WithdrawSolver): raise TypeError(f"Solver must be Bitcoin WithdrawSolver, not {type(solver).__name__} type.") # Set transaction fee, type, network and transaction self._fee, self._type, self._network, self._transaction = ( loaded_transaction_raw["fee"], loaded_transaction_raw["type"], loaded_transaction_raw["network"], MutableTransaction.unhexlify(loaded_transaction_raw["raw"]) ) # Sign withdraw transaction self._transaction.spend([TxOut( value=loaded_transaction_raw["outputs"]["value"], n=loaded_transaction_raw["outputs"]["tx_output_n"], script_pubkey=P2shScript.unhexlify( hex_string=loaded_transaction_raw["outputs"]["script"] ) )], [P2shSolver( redeem_script=solver.witness( network=self._network ), redeem_script_solver=solver.solve( network=self._network ) )]) # Encode withdraw transaction raw self._type = "bitcoin_withdraw_signed" self._signed_raw = b64encode(str(json.dumps(dict( raw=self._transaction.hexlify(), fee=self._fee, network=self._network, type=self._type, ))).encode()).decode() return self
def sign(self, transaction_raw: str, solver: RefundSolver) -> "RefundSignature": """ Sign unsigned refund transaction raw. :param transaction_raw: Bitcoin unsigned refund transaction raw. :type transaction_raw: str :param solver: Bitcoin refund solver. :type solver: bitcoin.solver.RefundSolver :returns: RefundSignature -- Bitcoin refund signature instance. >>> from swap.providers.bitcoin.signature import Signature >>> from swap.providers.bitcoin.solver import RefundSolver >>> unsigned_refund_transaction_raw: str = "eyJmZWUiOiA1NzYsICJyYXciOiAiMDIwMDAwMDAwMTMxZmI3NmEwYzM4ZDU3MzgxYjMxMTBlNGY1ZWU5YjUyODFkY2YyZmJlMmZlMjU2OTI2NmI3NTEwMTFkMjExYTIwMDAwMDAwMDAwZmZmZmZmZmYwMTYwODQwMTAwMDAwMDAwMDAxOTc2YTkxNGUwMGZmMmE2NDBiN2NlMmQzMzY4NjA3MzkxNjk0ODdhNTdmODRiMTU4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IHsidmFsdWUiOiAxMDAwMDAsICJ0eF9vdXRwdXRfbiI6IDAsICJzY3JpcHQiOiAiYTkxNGM4Yzc3YTliNDNlZTJiZGYxYTA3YzQ4Njk5ODMzZDc2NjhiZjI2NGM4NyJ9LCAibmV0d29yayI6ICJ0ZXN0bmV0IiwgInR5cGUiOiAiYml0Y29pbl9yZWZ1bmRfdW5zaWduZWQifQ" >>> bytecode: str = "63aa20821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e01588876a9140a0a6590e6ba4b48118d21b86812615219ece76b88ac67040ec4d660b17576a914e00ff2a640b7ce2d336860739169487a57f84b1588ac68" >>> refund_solver: RefundSolver = RefundSolver(xprivate_key="tprv8ZgxMBicQKsPeMHMJAc6uWGYiGqi1MVM2ybmzXL2TAoDpQe85uyDpdT7mv7Nhdu5rTCBEKLZsd9KyP2LQZJzZTvgVQvENArgU8e6DoYBiXf", bytecode=bytecode, endtime=1624687630) >>> refund_signature: RefundSignature = RefundSignature(network="testnet") >>> refund_signature.sign(transaction_raw=unsigned_refund_transaction_raw, solver=refund_solver) <swap.providers.bitcoin.signature.RefundSignature object at 0x0409DAF0> """ if not is_transaction_raw(transaction_raw=transaction_raw): raise TransactionRawError("Invalid Bitcoin unsigned transaction raw.") transaction_raw = clean_transaction_raw(transaction_raw) decoded_transaction_raw = b64decode(transaction_raw.encode()) loaded_transaction_raw = json.loads(decoded_transaction_raw.decode()) if not loaded_transaction_raw["type"] == "bitcoin_refund_unsigned": raise TypeError(f"Invalid Bitcoin refund unsigned transaction raw type, " f"you can't sign {loaded_transaction_raw['type']} type by using refund signature.") # Check parameter instances if not isinstance(solver, RefundSolver): raise TypeError(f"Solver must be Bitcoin RefundSolver, not {type(solver).__name__} type.") # Set transaction fee, type, network and transaction self._fee, self._type, self._network, self._transaction = ( loaded_transaction_raw["fee"], loaded_transaction_raw["type"], loaded_transaction_raw["network"], MutableTransaction.unhexlify(loaded_transaction_raw["raw"]) ) # Sign refund transaction self._transaction.spend([TxOut( value=loaded_transaction_raw["outputs"]["value"], n=loaded_transaction_raw["outputs"]["tx_output_n"], script_pubkey=P2shScript.unhexlify( hex_string=loaded_transaction_raw["outputs"]["script"] ) )], [P2shSolver( redeem_script=solver.witness( network=self._network ), redeem_script_solver=solver.solve( network=self._network ) )]) # Encode refund transaction raw self._type = "bitcoin_refund_signed" self._signed_raw = b64encode(str(json.dumps(dict( raw=self._transaction.hexlify(), fee=self._fee, network=self._network, type=self._type, ))).encode()).decode() return self
def build_transaction( self, address: str, recipients: dict, unit: str = config["unit"], locktime: int = config["locktime"]) -> "NormalTransaction": """ Build Bitcoin normal transaction. :param address: Bitcoin sender address. :type address: str :param recipients: Recipients Bitcoin address and amount. :type recipients: dict :param unit: Bitcoin unit, default to ``Satoshi``. :type unit: str :param locktime: Bitcoin transaction lock time, defaults to ``0``. :type locktime: int :returns: NormalTransaction -- Bitcoin normal transaction instance. >>> from swap.providers.bitcoin.transaction import NormalTransaction >>> normal_transaction: NormalTransaction = NormalTransaction(network="testnet") >>> normal_transaction.build_transaction(address="mkFWGt4hT11XS8dJKzzRFsTrqjjAwZfQAC", recipients={"2N6kHwQy6Ph5EdKNgzGrcW2WhGHKGfmP5ae": 10000000}, locktime=0) <swap.providers.bitcoin.transaction.NormalTransaction object at 0x0409DAF0> """ # Check parameter instances if not is_address(address, self._network): raise AddressError( f"Invalid Bitcoin sender '{address}' {self._network} address.") if unit not in ["BTC", "mBTC", "Satoshi"]: raise UnitError( "Invalid Bitcoin unit, choose only 'BTC', 'mBTC' or 'Satoshi' units." ) self._address, outputs, self._amount = (address, [], ( sum(recipients.values()) if unit == "Satoshi" else amount_unit_converter( amount=sum(recipients.values()), unit_from=f"{unit}2Satoshi"))) # Get Sender UTXO's self._utxos = get_utxos(address=self._address, network=self._network) # Outputs action for _address, _amount in recipients.items(): if not is_address(_address, self._network): raise AddressError( f"Invalid Bitcoin recipients '{_address}' {self._network} address." ) outputs.append( TxOut(value=int(_amount), n=len(outputs), script_pubkey=get_address_hash(address=_address, script=True))) # Get previous transaction indexes self._previous_transaction_indexes, max_amount = _get_previous_transaction_indexes( utxos=self._utxos, amount=self._amount, transaction_output=len(outputs)) # Build transaction inputs inputs, amount = _build_inputs( utxos=self._utxos, previous_transaction_indexes=self._previous_transaction_indexes) # Calculate the fee self._fee = fee_calculator(len(inputs), len(outputs)) if amount < self._amount: raise BalanceError("Insufficient spend UTXO's", "you don't have enough amount.") elif amount < (self._amount + self._fee): raise BalanceError( f"You don't have enough amount to pay '{self._fee}' Satoshi fee", f"you can spend maximum '{amount - self._fee}' Satoshi amount." ) return_amount: int = int(amount - (self._amount + self._fee)) if return_amount != 0: outputs.append( TxOut(value=return_amount, n=len(outputs), script_pubkey=get_address_hash(address=self._address, script=True))) # Build mutable transaction self._transaction = MutableTransaction(version=self._version, ins=inputs, outs=outputs, locktime=Locktime(locktime)) # Set transaction type self._type = "bitcoin_normal_unsigned" return self
def sign(self, transaction_raw: str, solver: FundSolver) -> "FundSignature": """ Sign unsigned fund transaction raw. :param transaction_raw: Bitcoin unsigned fund transaction raw. :type transaction_raw: str :param solver: Bitcoin fund solver. :type solver: bitcoin.solver.FundSolver :returns: FundSignature -- Bitcoin fund signature instance. >>> from swap.providers.bitcoin.signature import FundSignature >>> from swap.providers.bitcoin.solver import FundSolver >>> unsigned_fund_transaction_raw: str = "eyJmZWUiOiAxMTIyLCAicmF3IjogIjAyMDAwMDAwMDIzMWZiNzZhMGMzOGQ1NzM4MWIzMTEwZTRmNWVlOWI1MjgxZGNmMmZiZTJmZTI1NjkyNjZiNzUxMDExZDIxMWEyMDEwMDAwMDAwMGZmZmZmZmZmMDgwYjgyZWVjMzMyOTk2YTQyMmFlNGYwODBmNzRlNTNmZDJjYTRmMDcwMTFkNDdjNTkwODUzZTFlMzA1ZmUxMTAxMDAwMDAwMDBmZmZmZmZmZjAyYTA4NjAxMDAwMDAwMDAwMDE3YTkxNGM4Yzc3YTliNDNlZTJiZGYxYTA3YzQ4Njk5ODMzZDc2NjhiZjI2NGM4NzMyOWMwZDAwMDAwMDAwMDAxOTc2YTkxNGUwMGZmMmE2NDBiN2NlMmQzMzY4NjA3MzkxNjk0ODdhNTdmODRiMTU4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IFt7InZhbHVlIjogOTQzMzAsICJ0eF9vdXRwdXRfbiI6IDEsICJzY3JpcHQiOiAiNzZhOTE0ZTAwZmYyYTY0MGI3Y2UyZDMzNjg2MDczOTE2OTQ4N2E1N2Y4NGIxNTg4YWMifSwgeyJ2YWx1ZSI6IDg5ODc0NiwgInR4X291dHB1dF9uIjogMSwgInNjcmlwdCI6ICI3NmE5MTRlMDBmZjJhNjQwYjdjZTJkMzM2ODYwNzM5MTY5NDg3YTU3Zjg0YjE1ODhhYyJ9XSwgIm5ldHdvcmsiOiAidGVzdG5ldCIsICJ0eXBlIjogImJpdGNvaW5fZnVuZF91bnNpZ25lZCJ9" >>> fund_solver: FundSolver = FundSolver(xprivate_key="tprv8ZgxMBicQKsPeMHMJAc6uWGYiGqi1MVM2ybmzXL2TAoDpQe85uyDpdT7mv7Nhdu5rTCBEKLZsd9KyP2LQZJzZTvgVQvENArgU8e6DoYBiXf") >>> fund_signature: FundSignature = FundSignature(network="testnet") >>> fund_signature.sign(transaction_raw=unsigned_fund_transaction_raw, solver=fund_solver) <swap.providers.bitcoin.signature.FundSignature object at 0x0409DAF0> """ if not is_transaction_raw(transaction_raw=transaction_raw): raise TransactionRawError("Invalid Bitcoin unsigned transaction raw.") transaction_raw = clean_transaction_raw(transaction_raw) decoded_transaction_raw = b64decode(transaction_raw.encode()) loaded_transaction_raw = json.loads(decoded_transaction_raw.decode()) if not loaded_transaction_raw["type"] == "bitcoin_fund_unsigned": raise TypeError(f"Invalid Bitcoin fund unsigned transaction raw type, " f"you can't sign {loaded_transaction_raw['type']} type by using fund signature.") # Check parameter instances if not isinstance(solver, FundSolver): raise TypeError(f"Solver must be Bitcoin FundSolver, not {type(solver).__name__} type.") # Set transaction fee, type, network and transaction self._fee, self._type, self._network, self._transaction = ( loaded_transaction_raw["fee"], loaded_transaction_raw["type"], loaded_transaction_raw["network"], MutableTransaction.unhexlify(loaded_transaction_raw["raw"]) ) # Organize outputs outputs = [] for output in loaded_transaction_raw["outputs"]: outputs.append(TxOut( value=output["value"], n=output["tx_output_n"], script_pubkey=Script.unhexlify( hex_string=output["script"] ) )) # Sign fund transaction self._transaction.spend( txouts=outputs, solvers=[solver.solve(network=self._network) for _ in outputs] ) # Encode fund transaction raw self._type = "bitcoin_fund_signed" self._signed_raw = b64encode(str(json.dumps(dict( raw=self._transaction.hexlify(), fee=self._fee, network=self._network, type=self._type ))).encode()).decode() return self
def test_all(self): global keys priv = PrivateKey.from_bip32(keys[0][1]) pk = priv.pub() addr_string = str(pk.to_address()) utxo = [] for i in range(3): # create 3 tx to add to UTXO txid = regtest.send_rpc_cmd(['sendtoaddress', addr_string, '100'], 0) to_spend = Transaction.unhexlify( regtest.send_rpc_cmd(['getrawtransaction', txid, '0'], 0)) txout = None for out in to_spend.outs: if str(out.script_pubkey.address()) == addr_string: txout = out break assert txout is not None utxo.append({ 'txid': txid, 'txout': txout, 'solver': P2pkhSolver(priv), 'next_seq': Sequence.max(), 'next_locktime': Locktime(0) }) regtest.send_rpc_cmd(['generate', '100'], 0) generate = False next_locktime = Locktime(0) next_sequence = Sequence.max() i = 0 # 1785 # 382 # 2376 # 1180 # while i < len(self.all) - 2: print('{:04d}\r'.format(i), end='') ins = [ MutableTxIn(unspent['txid'], unspent['txout'].n, ScriptSig.empty(), unspent['next_seq']) for unspent in utxo ] outs = [] prev_types = [] for j, (unspent, script) in enumerate(zip(utxo, self.all[i:i + 3])): outs.append( TxOut(unspent['txout'].value - 1000000, j, script[0])) prev_types.append(script[2]) tx = MutableTransaction( 2, ins, outs, min_locktime(unspent['next_locktime'] for unspent in utxo)) tx = tx.spend([unspent['txout'] for unspent in utxo], [unspent['solver'] for unspent in utxo]) # print('====================') # print('txid: {}'.format(tx.txid)) # print() # print(tx) # print() # print('raw: {}'.format(tx.hexlify())) # print('prev_scripts, amounts, solvers:') for unspent in utxo: if isinstance(unspent['solver'], P2shSolver): if isinstance(unspent['solver'].redeem_script_solver, P2wshV0Solver): prev = unspent[ 'solver'].redeem_script_solver.witness_script else: prev = unspent['solver'].redeem_script elif isinstance(unspent['solver'], P2wshV0Solver): prev = unspent['solver'].witness_script else: prev = unspent['txout'].script_pubkey print(prev, unspent['txout'].value, unspent['solver'].__class__.__name__) regtest.send_rpc_cmd(['sendrawtransaction', tx.hexlify()], 0) utxo = [] for j, (output, prev_type) in enumerate(zip(tx.outs, prev_types)): if 'time' in prev_type: if 'absolute' in prev_type: next_locktime = Locktime(100) next_sequence = Sequence(0xfffffffe) if 'relative' in prev_type: next_sequence = Sequence(3) generate = True else: next_locktime = Locktime(0) next_sequence = Sequence.max() utxo.append({ 'txid': tx.txid, 'txout': output, 'solver': self.all[i + j][1][0], # solver 'next_seq': next_sequence, 'next_locktime': next_locktime }) if generate: regtest.send_rpc_cmd(['generate', '4'], 0) generate = False if not i % 10: regtest.send_rpc_cmd(['generate', '2'], 0) i += 1 ins = [ MutableTxIn(unspent['txid'], unspent['txout'].n, ScriptSig.empty(), unspent['next_seq']) for unspent in utxo ] tx = MutableTransaction( 2, ins, [ TxOut( sum(unspent['txout'].value for unspent in utxo) - 1000000, 0, self.final['script']) ], min_locktime(unspent['next_locktime'] for unspent in utxo)) tx = tx.spend([unspent['txout'] for unspent in utxo], [unspent['solver'] for unspent in utxo]) # print('====================') # print('txid: {}'.format(tx.txid)) # print() # print(tx) # print() # print('raw: {}'.format(tx.hexlify())) # print('prev_scripts, amounts, solvers:') for unspent in utxo: print(unspent['txout'].script_pubkey, unspent['txout'].value, unspent['solver'].__class__.__name__) regtest.send_rpc_cmd(['sendrawtransaction', tx.hexlify()], 0) regtest.teardown()
# 设置罚金 penalty = 100000 assert penalty + mining_fee <= balance, 'committer账户余额不足' # 创建交易 to_spend_raw = get_raw_tx(to_spend_hash, coin_symbol) to_spend = TransactionFactory.unhexlify(to_spend_raw) unsigned = MutableTransaction(version=2, ins=[ TxIn(txid=to_spend.txid, txout=0, script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=[ TxOut(value=penalty, n=0, script_pubkey=lock_time_script), TxOut(value=balance - penalty - mining_fee, n=1, script_pubkey=change_script) ], locktime=Locktime(0)) # 输入脚本 solver = P2pkhSolver(privk) # 修改交易 signed = unsigned.spend([to_spend.outs[0]], [solver]) print('commit_tx_hex: ', signed.hexlify()) # 发送交易
def build_transaction(self, transaction_id, wallet, amount, locktime=0): """ Build Bitcoin refund transaction. :param transaction_id: Bitcoin fund transaction id to redeem. :type transaction_id: str :param wallet: Bitcoin sender wallet. :type wallet: bitcoin.wallet.Wallet :param amount: Bitcoin amount to withdraw. :type amount: int :param locktime: Bitcoin transaction lock time, defaults to 0. :type locktime: int :returns: RefundTransaction -- Bitcoin refund transaction instance. >>> from shuttle.providers.bitcoin.transaction import RefundTransaction >>> from shuttle.providers.bitcoin.wallet import Wallet >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett") >>> refund_transaction = RefundTransaction(network="testnet") >>> refund_transaction.build_transaction(transaction_id="1006a6f537fcc4888c65f6ff4f91818a1c6e19bdd3130f59391c00212c552fbd", wallet=sender_wallet, amount=10000) <shuttle.providers.bitcoin.transaction.RefundTransaction object at 0x0409DAF0> """ # Checking parameter instances if not isinstance(transaction_id, str): raise TypeError("invalid amount instance, only takes string type") if not isinstance(wallet, Wallet): raise TypeError( "invalid wallet instance, only takes Bitcoin Wallet class") # Setting transaction_id and wallet self.transaction_id, self.wallet = transaction_id, wallet # Getting transaction detail by id self.transaction_detail = get_transaction_detail(self.transaction_id) # Getting Hash time lock contract output detail self.htlc_detail = self.transaction_detail["outputs"][0] # Getting HTLC funded amount balance htlc_amount = self.htlc_detail["value"] # Calculating fee self._fee = fee_calculator(1, 1) if amount < self._fee: raise BalanceError("insufficient spend utxos") elif not htlc_amount >= (amount - self._fee): raise BalanceError("insufficient spend utxos", f"maximum you can withdraw {htlc_amount}") # Building mutable Bitcoin transaction self.transaction = MutableTransaction( version=self.version, ins=[ TxIn(txid=self.transaction_id, txout=0, script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=[ TxOut(value=(amount - self._fee), n=0, script_pubkey=P2pkhScript.unhexlify( hex_string=self.wallet.p2pkh())) ], locktime=Locktime(locktime)) self._type = "bitcoin_refund_unsigned" return self
class FundTransaction(Transaction): """ Bitcoin Fund transaction. :param network: Bitcoin network, defaults to ``mainnet``. :type network: str :param version: Bitcoin transaction version, defaults to ``2``. :type version: int :returns: FundTransaction -- Bitcoin fund transaction instance. .. warning:: Do not forget to build transaction after initialize fund transaction. """ def __init__(self, network: str = config["network"], version: int = config["version"]): super().__init__(network=network, version=version) self._htlc: Optional[HTLC] = None self._utxos: Optional[list] = None self._previous_transaction_indexes: Optional[list] = None self._interest: Optional[int] = None def build_transaction( self, address: str, htlc: HTLC, amount: Optional[Union[int, float]], unit: str = config["unit"], locktime: int = config["locktime"]) -> "FundTransaction": """ Build Bitcoin fund transaction. :param address: Bitcoin sender address. :type address: str :param htlc: Bitcoin HTLC instance. :type htlc: bitcoin.htlc.HTLC :param amount: Bitcoin amount, default to ``None``. :type amount: int, float :param unit: Bitcoin unit, default to ``Satoshi``. :type unit: str :param locktime: Bitcoin transaction lock time, defaults to ``0``. :type locktime: int :returns: FundTransaction -- Bitcoin fund transaction instance. >>> from swap.providers.bitcoin.htlc import HTLC >>> from swap.providers.bitcoin.transaction import FundTransaction >>> from swap.utils import sha256 >>> htlc: HTLC = HTLC(network="testnet") >>> htlc.build_htlc(secret_hash=sha256("Hello Meheret!"), recipient_address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", sender_address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", endtime=1624687630) >>> fund_transaction: FundTransaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", htlc=htlc, amount=0.001, unit="BTC") <swap.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Check parameter instances if not is_address(address, self._network): raise AddressError( f"Invalid Bitcoin sender '{address}' {self._network} address.") if not isinstance(htlc, HTLC): raise TypeError( "Invalid Bitcoin HTLC instance, only takes xinfin HTLC class") if htlc.agreements and address != htlc.agreements["sender_address"]: raise AddressError( f"Wrong Bitcoin sender '{address}' address", "address must be equal with HTLC agreements sender address.") if unit not in ["BTC", "mBTC", "Satoshi"]: raise UnitError( "Invalid Bitcoin unit, choose only 'BTC', 'mBTC' or 'Satoshi' units." ) self._address, outputs, self._htlc, self._amount = ( address, [], htlc, (amount if unit == "Satoshi" else amount_unit_converter( amount=amount, unit_from=f"{unit}2Satoshi"))) # Get Sender UTXO's self._utxos = get_utxos(address=self._address, network=self._network) # Outputs action outputs.append( TxOut(value=self._amount, n=0, script_pubkey=get_address_hash( address=self._htlc.contract_address(), script=True))) # Get previous transaction indexes self._previous_transaction_indexes, max_amount = _get_previous_transaction_indexes( utxos=self._utxos, amount=self._amount, transaction_output=2) # Build transaction inputs inputs, amount = _build_inputs( utxos=self._utxos, previous_transaction_indexes=self._previous_transaction_indexes) # Calculate the fee self._fee = fee_calculator(len(inputs), 2) if amount < self._amount: raise BalanceError("Insufficient spend UTXO's", "you don't have enough amount.") elif amount < (self._amount + self._fee): raise BalanceError( f"You don't have enough amount to pay '{self._fee}' Satoshi fee", f"you can spend maximum '{amount - self._fee}' Satoshi amount." ) return_amount: int = int(amount - (self._amount + self._fee)) if return_amount != 0: outputs.append( TxOut(value=return_amount, n=len(outputs), script_pubkey=get_address_hash(address=self._address, script=True))) # Build mutable transaction self._transaction = MutableTransaction(version=self._version, ins=inputs, outs=outputs, locktime=Locktime(locktime)) # Set transaction type self._type = "bitcoin_fund_unsigned" return self def sign(self, solver: FundSolver) -> "FundTransaction": """ Sign Bitcoin fund transaction. :param solver: Bitcoin fund solver. :type solver: bitcoin.solver.FundSolver :returns: FundTransaction -- Bitcoin fund transaction instance. >>> from swap.providers.bitcoin.htlc import HTLC >>> from swap.providers.bitcoin.transaction import FundTransaction >>> from swap.providers.bitcoin.solver import FundSolver >>> from swap.utils import sha256 >>> htlc: HTLC = HTLC(network="testnet") >>> htlc.build_htlc(secret_hash=sha256("Hello Meheret!"), recipient_address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", sender_address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", endtime=1624687630) >>> fund_transaction: FundTransaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", htlc=htlc, amount=0.001, unit="BTC") >>> fund_solver: FundSolver = FundSolver(xprivate_key="tprv8ZgxMBicQKsPeMHMJAc6uWGYiGqi1MVM2ybmzXL2TAoDpQe85uyDpdT7mv7Nhdu5rTCBEKLZsd9KyP2LQZJzZTvgVQvENArgU8e6DoYBiXf") >>> fund_transaction.sign(solver=fund_solver) <swap.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Check parameter instances if not isinstance(solver, FundSolver): raise TypeError( f"Solver must be Bitcoin FundSolver, not {type(solver).__name__} type." ) if self._transaction is None: raise ValueError("Transaction is none, build transaction first.") # Organize outputs outputs = _build_outputs( utxos=self._utxos, previous_transaction_indexes=self._previous_transaction_indexes) # Sign fund transaction self._transaction.spend( txouts=outputs, solvers=[solver.solve(network=self._network) for _ in outputs]) # Set transaction type self._type = "bitcoin_fund_signed" return self def transaction_raw(self) -> str: """ Get Bitcoin fund transaction raw. :returns: str -- Bitcoin fund transaction raw. >>> from swap.providers.bitcoin.htlc import HTLC >>> from swap.providers.bitcoin.transaction import FundTransaction >>> from swap.utils import sha256 >>> htlc: HTLC = HTLC(network="testnet") >>> htlc.build_htlc(secret_hash=sha256("Hello Meheret!"), recipient_address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", sender_address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", endtime=1624687630) >>> fund_transaction: FundTransaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", htlc=htlc, amount=0.001, unit="BTC") >>> fund_transaction.transaction_raw() "eyJmZWUiOiAxMTIyLCAicmF3IjogIjAyMDAwMDAwMDIzMWZiNzZhMGMzOGQ1NzM4MWIzMTEwZTRmNWVlOWI1MjgxZGNmMmZiZTJmZTI1NjkyNjZiNzUxMDExZDIxMWEyMDEwMDAwMDAwMGZmZmZmZmZmMDgwYjgyZWVjMzMyOTk2YTQyMmFlNGYwODBmNzRlNTNmZDJjYTRmMDcwMTFkNDdjNTkwODUzZTFlMzA1ZmUxMTAxMDAwMDAwMDBmZmZmZmZmZjAyYTA4NjAxMDAwMDAwMDAwMDE3YTkxNGM4Yzc3YTliNDNlZTJiZGYxYTA3YzQ4Njk5ODMzZDc2NjhiZjI2NGM4NzMyOWMwZDAwMDAwMDAwMDAxOTc2YTkxNGUwMGZmMmE2NDBiN2NlMmQzMzY4NjA3MzkxNjk0ODdhNTdmODRiMTU4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IFt7InZhbHVlIjogOTQzMzAsICJ0eF9vdXRwdXRfbiI6IDEsICJzY3JpcHQiOiAiNzZhOTE0ZTAwZmYyYTY0MGI3Y2UyZDMzNjg2MDczOTE2OTQ4N2E1N2Y4NGIxNTg4YWMifSwgeyJ2YWx1ZSI6IDg5ODc0NiwgInR4X291dHB1dF9uIjogMSwgInNjcmlwdCI6ICI3NmE5MTRlMDBmZjJhNjQwYjdjZTJkMzM2ODYwNzM5MTY5NDg3YTU3Zjg0YjE1ODhhYyJ9XSwgIm5ldHdvcmsiOiAidGVzdG5ldCIsICJ0eXBlIjogImJpdGNvaW5fZnVuZF91bnNpZ25lZCJ9" """ # Check parameter instances if self._transaction is None: raise ValueError("Transaction is none, build transaction first.") # Encode fund transaction raw if self._type == "bitcoin_fund_signed": return clean_transaction_raw( b64encode( str( json.dumps( dict( raw=self._transaction.hexlify(), fee=self._fee, network=self._network, type=self._type, ))).encode()).decode()) return clean_transaction_raw( b64encode( str( json.dumps( dict( fee=self._fee, raw=self._transaction.hexlify(), outputs=_build_outputs( utxos=self._utxos, previous_transaction_indexes=self. _previous_transaction_indexes, only_dict=True), network=self._network, type=self._type, ))).encode()).decode())
def build_transaction(self, wallet, htlc, amount, locktime=0): """ Build Bitcoin fund transaction. :param wallet: Bitcoin sender wallet. :type wallet: bitcoin.wallet.Wallet :param htlc: Bitcoin hash time lock contract (HTLC). :type htlc: bitcoin.htlc.HTLC :param amount: Bitcoin amount to fund. :type amount: int :param locktime: Bitcoin transaction lock time, defaults to 0. :type locktime: int :returns: FundTransaction -- Bitcoin fund transaction instance. >>> from shuttle.providers.bitcoin.htlc import HTLC >>> from shuttle.providers.bitcoin.transaction import FundTransaction >>> from shuttle.providers.bitcoin.wallet import Wallet >>> htlc = HTLC(network="testnet").init("821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e0158", "muTnffLDR5LtFeLR2i3WsKVfdyvzfyPnVB", "mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q", 1000) >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett") >>> fund_transaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(wallet=sender_wallet, htlc=htlc, amount=10000) <shuttle.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Checking parameter instances if not isinstance(wallet, Wallet): raise TypeError( "invalid wallet instance, only takes Bitcoin Wallet class") if not isinstance(htlc, HTLC): raise TypeError( "invalid htlc instance, only takes Bitcoin HTLC class") if not isinstance(amount, int): raise TypeError("invalid amount instance, only takes integer type") # Setting wallet, htlc, amount and unspent self.wallet, self.htlc, self.amount = wallet, htlc, amount # Getting unspent transaction output self.unspent = self.wallet.unspent() # Setting previous transaction indexes self.previous_transaction_indexes = \ self.get_previous_transaction_indexes(amount=self.amount) # Getting transaction inputs and amount inputs, amount = self.inputs( utxos=self.unspent, previous_transaction_indexes=self.previous_transaction_indexes) # Calculating Bitcoin fee self._fee = fee_calculator(len(inputs), 2) if amount < (self.amount + self._fee): raise BalanceError("insufficient spend utxos") # Building mutable Bitcoin transaction self.transaction = MutableTransaction( version=self.version, ins=inputs, outs=[ # Funding into hash time lock contract script hash TxOut(value=self.amount, n=0, script_pubkey=P2shScript.unhexlify( hex_string=self.htlc.hash())), # Controlling amounts when we are funding on htlc script TxOut(value=(amount - (self._fee + self.amount)), n=1, script_pubkey=P2pkhScript.unhexlify( hex_string=self.wallet.p2pkh())) ], locktime=Locktime(locktime)) self._type = "bitcoin_fund_unsigned" return self
class RefundTransaction(Transaction): """ Bitcoin RefundTransaction class. :param version: Bitcoin transaction version, defaults to 2. :type version: int :param network: Bitcoin network, defaults to testnet. :type network: str :returns: Transaction -- Bitcoin transaction instance. .. warning:: Do not forget to build transaction after initialize refund transaction. """ # Initialization claim transaction def __init__(self, version=2, network="testnet"): super().__init__(version=version, network=network) # Initialization transaction_id, sender wallet and amount self.transaction_id, self.wallet, \ self.amount = None, None, None # Get transaction detail self.transaction_detail = None # Transaction detail outputs (HTLC and Sender account) self.htlc_detail, self.sender_detail = None, None # Building transaction def build_transaction(self, transaction_id, wallet, amount, locktime=0): """ Build Bitcoin refund transaction. :param transaction_id: Bitcoin fund transaction id to redeem. :type transaction_id: str :param wallet: Bitcoin sender wallet. :type wallet: bitcoin.wallet.Wallet :param amount: Bitcoin amount to withdraw. :type amount: int :param locktime: Bitcoin transaction lock time, defaults to 0. :type locktime: int :returns: RefundTransaction -- Bitcoin refund transaction instance. >>> from shuttle.providers.bitcoin.transaction import RefundTransaction >>> from shuttle.providers.bitcoin.wallet import Wallet >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett") >>> refund_transaction = RefundTransaction(network="testnet") >>> refund_transaction.build_transaction(transaction_id="1006a6f537fcc4888c65f6ff4f91818a1c6e19bdd3130f59391c00212c552fbd", wallet=sender_wallet, amount=10000) <shuttle.providers.bitcoin.transaction.RefundTransaction object at 0x0409DAF0> """ # Checking parameter instances if not isinstance(transaction_id, str): raise TypeError("invalid amount instance, only takes string type") if not isinstance(wallet, Wallet): raise TypeError( "invalid wallet instance, only takes Bitcoin Wallet class") # Setting transaction_id and wallet self.transaction_id, self.wallet = transaction_id, wallet # Getting transaction detail by id self.transaction_detail = get_transaction_detail(self.transaction_id) # Getting Hash time lock contract output detail self.htlc_detail = self.transaction_detail["outputs"][0] # Getting HTLC funded amount balance htlc_amount = self.htlc_detail["value"] # Calculating fee self._fee = fee_calculator(1, 1) if amount < self._fee: raise BalanceError("insufficient spend utxos") elif not htlc_amount >= (amount - self._fee): raise BalanceError("insufficient spend utxos", f"maximum you can withdraw {htlc_amount}") # Building mutable Bitcoin transaction self.transaction = MutableTransaction( version=self.version, ins=[ TxIn(txid=self.transaction_id, txout=0, script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=[ TxOut(value=(amount - self._fee), n=0, script_pubkey=P2pkhScript.unhexlify( hex_string=self.wallet.p2pkh())) ], locktime=Locktime(locktime)) self._type = "bitcoin_refund_unsigned" return self # Signing refund transaction def sign(self, solver): """ Sign Bitcoin refund transaction. :param solver: Bitcoin refund solver. :type solver: bitcoin.solver.RefundSolver :returns: RefundTransaction -- Bitcoin refund transaction instance. >>> from shuttle.providers.bitcoin.transaction import RefundTransaction >>> from shuttle.providers.bitcoin.solver import RefundSolver >>> from shuttle.providers.bitcoin.wallet import Wallet >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett1234") >>> refund_solver = RefundSolver(sender_wallet.private_key(), "3a26da82ead15a80533a02696656b14b5dbfd84eb14790f2e1be5e9e45820eeb", "muTnffLDR5LtFeLR2i3WsKVfdyvzfyPnVB", sender_wallet.address(), 1000) >>> refund_transaction = RefundTransaction(network="testnet") >>> refund_transaction.build_transaction("1006a6f537fcc4888c65f6ff4f91818a1c6e19bdd3130f59391c00212c552fbd", sender_wallet, 10000) >>> refund_transaction.sign(solver=refund_solver) <shuttle.providers.bitcoin.transaction.RefundTransaction object at 0x0409DAF0> """ # Checking parameter instances if not isinstance(solver, RefundSolver): raise TypeError( "invalid solver instance, only takes Bitcoin RefundSolver class" ) if self.transaction is None: raise ValueError( "transaction script is none, build transaction first") self.transaction.spend([ TxOut(value=self.htlc_detail["value"], n=0, script_pubkey=P2shScript.unhexlify( hex_string=self.htlc_detail["script"])) ], [ P2shSolver(redeem_script=solver.witness(network=self.network), redeem_script_solver=solver.solve()) ]) self._type = "bitcoin_refund_signed" return self def unsigned_raw(self): """ Get Bitcoin unsigned refund transaction raw. :returns: str -- Bitcoin unsigned refund transaction raw. >>> from shuttle.providers.bitcoin.transaction import RefundTransaction >>> from shuttle.providers.bitcoin.wallet import Wallet >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett1234") >>> refund_transaction = RefundTransaction(network="testnet") >>> refund_transaction.build_transaction("1006a6f537fcc4888c65f6ff4f91818a1c6e19bdd3130f59391c00212c552fbd", sender_wallet, 10000) >>> refund_transaction.unsigned_raw() "eyJmZWUiOiA2NzgsICJyYXciOiAiMDIwMDAwMDAwMTJjMzkyMjE3NDgzOTA2ZjkwMmU3M2M0YmMxMzI4NjRkZTU4MTUzNzcyZDc5MjY4OTYwOTk4MTYyMjY2NjM0YmUwMTAwMDAwMDAwZmZmZmZmZmYwMmU4MDMwMDAwMDAwMDAwMDAxN2E5MTQ5NzE4OTRjNThkODU5ODFjMTZjMjA1OWQ0MjJiY2RlMGIxNTZkMDQ0ODdhNjI5MDAwMDAwMDAwMDAwMTk3NmE5MTQ2YmNlNjVlNThhNTBiOTc5ODk5MzBlOWE0ZmYxYWMxYTc3NTE1ZWYxODhhYzAwMDAwMDAwIiwgIm91dHB1dHMiOiBbeyJhbW91bnQiOiAxMjM0MCwgIm4iOiAxLCAic2NyaXB0IjogIjc2YTkxNDZiY2U2NWU1OGE1MGI5Nzk4OTkzMGU5YTRmZjFhYzFhNzc1MTVlZjE4OGFjIn1dLCAidHlwZSI6ICJiaXRjb2luX2Z1bmRfdW5zaWduZWQifQ" """ # Checking transaction if self.transaction is None: raise ValueError( "transaction script is none, build transaction first") # Encoding refund transaction raw return b64encode( str( json.dumps( dict(fee=self._fee, raw=self.transaction.hexlify(), outputs=dict( value=self.htlc_detail["value"], n=0, script_pubkey=self.htlc_detail["script"]), network=self.network, type="bitcoin_refund_unsigned"))).encode()).decode()
def build_transaction( self, address: str, htlc: HTLC, amount: Optional[Union[int, float]], unit: str = config["unit"], locktime: int = config["locktime"]) -> "FundTransaction": """ Build Bitcoin fund transaction. :param address: Bitcoin sender address. :type address: str :param htlc: Bitcoin HTLC instance. :type htlc: bitcoin.htlc.HTLC :param amount: Bitcoin amount, default to ``None``. :type amount: int, float :param unit: Bitcoin unit, default to ``Satoshi``. :type unit: str :param locktime: Bitcoin transaction lock time, defaults to ``0``. :type locktime: int :returns: FundTransaction -- Bitcoin fund transaction instance. >>> from swap.providers.bitcoin.htlc import HTLC >>> from swap.providers.bitcoin.transaction import FundTransaction >>> from swap.utils import sha256 >>> htlc: HTLC = HTLC(network="testnet") >>> htlc.build_htlc(secret_hash=sha256("Hello Meheret!"), recipient_address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", sender_address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", endtime=1624687630) >>> fund_transaction: FundTransaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", htlc=htlc, amount=0.001, unit="BTC") <swap.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Check parameter instances if not is_address(address, self._network): raise AddressError( f"Invalid Bitcoin sender '{address}' {self._network} address.") if not isinstance(htlc, HTLC): raise TypeError( "Invalid Bitcoin HTLC instance, only takes xinfin HTLC class") if htlc.agreements and address != htlc.agreements["sender_address"]: raise AddressError( f"Wrong Bitcoin sender '{address}' address", "address must be equal with HTLC agreements sender address.") if unit not in ["BTC", "mBTC", "Satoshi"]: raise UnitError( "Invalid Bitcoin unit, choose only 'BTC', 'mBTC' or 'Satoshi' units." ) self._address, outputs, self._htlc, self._amount = ( address, [], htlc, (amount if unit == "Satoshi" else amount_unit_converter( amount=amount, unit_from=f"{unit}2Satoshi"))) # Get Sender UTXO's self._utxos = get_utxos(address=self._address, network=self._network) # Outputs action outputs.append( TxOut(value=self._amount, n=0, script_pubkey=get_address_hash( address=self._htlc.contract_address(), script=True))) # Get previous transaction indexes self._previous_transaction_indexes, max_amount = _get_previous_transaction_indexes( utxos=self._utxos, amount=self._amount, transaction_output=2) # Build transaction inputs inputs, amount = _build_inputs( utxos=self._utxos, previous_transaction_indexes=self._previous_transaction_indexes) # Calculate the fee self._fee = fee_calculator(len(inputs), 2) if amount < self._amount: raise BalanceError("Insufficient spend UTXO's", "you don't have enough amount.") elif amount < (self._amount + self._fee): raise BalanceError( f"You don't have enough amount to pay '{self._fee}' Satoshi fee", f"you can spend maximum '{amount - self._fee}' Satoshi amount." ) return_amount: int = int(amount - (self._amount + self._fee)) if return_amount != 0: outputs.append( TxOut(value=return_amount, n=len(outputs), script_pubkey=get_address_hash(address=self._address, script=True))) # Build mutable transaction self._transaction = MutableTransaction(version=self._version, ins=inputs, outs=outputs, locktime=Locktime(locktime)) # Set transaction type self._type = "bitcoin_fund_unsigned" return self
class WithdrawTransaction(Transaction): """ Bitcoin Withdraw transaction. :param network: Bitcoin network, defaults to ``mainnet``. :type network: str :param version: Bitcoin transaction version, defaults to ``2``. :type version: int :returns: WithdrawTransaction -- Bitcoin withdraw transaction instance. .. warning:: Do not forget to build transaction after initialize withdraw transaction. """ def __init__(self, network: str = config["network"], version: int = config["version"]): super().__init__(network=network, version=version) self._transaction_hash: Optional[str] = None self._transaction_detail: Optional[dict] = None self._htlc_utxo: Optional[dict] = None self._interest: Optional[int] = None def build_transaction( self, address: str, transaction_hash: str, locktime: int = config["locktime"]) -> "WithdrawTransaction": """ Build Bitcoin withdraw transaction. :param address: Bitcoin recipient address. :type address: str :param transaction_hash: Bitcoin funded transaction hash/id. :type transaction_hash: str :param locktime: Bitcoin transaction lock time, defaults to ``0``. :type locktime: int :returns: WithdrawTransaction -- Bitcoin withdraw transaction instance. >>> from swap.providers.bitcoin.transaction import WithdrawTransaction >>> withdraw_transaction: WithdrawTransaction = WithdrawTransaction("testnet") >>> withdraw_transaction.build_transaction(address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") <swap.providers.bitcoin.transaction.WithdrawTransaction object at 0x0409DAF0> """ # Check parameter instances if not is_address(address, self._network): raise AddressError( f"Invalid Bitcoin recipient '{address}' {self._network} address." ) # Set address and transaction hash self._address, self._transaction_hash, = address, transaction_hash # Get transaction self._transaction_detail = get_transaction( transaction_hash=self._transaction_hash, network=self._network) # Find HTLC UTXO self._htlc_utxo = find_p2sh_utxo(transaction=self._transaction_detail) if self._htlc_utxo is None: raise ValueError( "Invalid transaction hash, there is no pay to script hash (P2SH) address." ) self._amount = self._htlc_utxo["value"] # Calculate the fee self._fee = fee_calculator(1, 1) outputs: list = [ TxOut(value=(self._amount - self._fee), n=0, script_pubkey=get_address_hash(address=self._address, script=True)) ] # Build mutable transaction self._transaction = MutableTransaction( version=self._version, ins=[ TxIn(txid=self._transaction_hash, txout=self._htlc_utxo["position"], script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=outputs, locktime=Locktime(locktime)) # Set transaction type self._type = "bitcoin_withdraw_unsigned" return self def sign(self, solver: WithdrawSolver) -> "WithdrawTransaction": """ Sign Bitcoin withdraw transaction. :param solver: Bitcoin withdraw solver. :type solver: bitcoin.solver.WithdrawSolver :returns: WithdrawTransaction -- Bitcoin withdraw transaction instance. >>> from swap.providers.bitcoin.transaction import WithdrawTransaction >>> from swap.providers.bitcoin.solver import WithdrawSolver >>> withdraw_transaction: WithdrawTransaction = WithdrawTransaction("testnet") >>> withdraw_transaction.build_transaction(address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") >>> bytecode: str = "63aa20821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e01588876a9140a0a6590e6ba4b48118d21b86812615219ece76b88ac67040ec4d660b17576a914e00ff2a640b7ce2d336860739169487a57f84b1588ac68" >>> withdraw_solver: WithdrawSolver = WithdrawSolver(xprivate_key="tprv8ZgxMBicQKsPf949JcuVFLXPJ5m4VKe33gVX3FYVZYVHr2dChU8K66aEQcPdHpUgACq5GQu81Z4e3QN1vxCrV4pxcUcXHoRTamXBRaPdJhW", secret_key="Hello Meheret!", bytecode=bytecode) >>> withdraw_transaction.sign(solver=withdraw_solver) <swap.providers.bitcoin.transaction.WithdrawTransaction object at 0x0409DAF0> """ # Check parameter instances if not isinstance(solver, WithdrawSolver): raise TypeError( f"Solver must be Bitcoin WithdrawSolver, not {type(solver).__name__} type." ) if self._transaction is None: raise ValueError("Transaction is none, build transaction first.") self._transaction.spend([ TxOut(value=self._htlc_utxo["value"], n=0, script_pubkey=P2shScript.unhexlify( hex_string=self._htlc_utxo["script"])) ], [ P2shSolver( redeem_script=solver.witness(network=self._network), redeem_script_solver=solver.solve(network=self._network)) ]) # Set transaction type self._type = "bitcoin_withdraw_signed" return self def transaction_raw(self) -> str: """ Get Bitcoin withdraw transaction raw. :returns: str -- Bitcoin withdraw transaction raw. >>> from swap.providers.bitcoin.transaction import WithdrawTransaction >>> withdraw_transaction: WithdrawTransaction = WithdrawTransaction("testnet") >>> withdraw_transaction.build_transaction(address="mgS3WMHp9nvdUPeDJxr5iCF2P5HuFZSR3V", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") >>> withdraw_transaction.transaction_raw() "eyJmZWUiOiA1NzYsICJyYXciOiAiMDIwMDAwMDAwMTMxZmI3NmEwYzM4ZDU3MzgxYjMxMTBlNGY1ZWU5YjUyODFkY2YyZmJlMmZlMjU2OTI2NmI3NTEwMTFkMjExYTIwMDAwMDAwMDAwZmZmZmZmZmYwMTYwODQwMTAwMDAwMDAwMDAxOTc2YTkxNDBhMGE2NTkwZTZiYTRiNDgxMThkMjFiODY4MTI2MTUyMTllY2U3NmI4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IHsidmFsdWUiOiAxMDAwMDAsICJ0eF9vdXRwdXRfbiI6IDAsICJzY3JpcHQiOiAiYTkxNGM4Yzc3YTliNDNlZTJiZGYxYTA3YzQ4Njk5ODMzZDc2NjhiZjI2NGM4NyJ9LCAibmV0d29yayI6ICJ0ZXN0bmV0IiwgInR5cGUiOiAiYml0Y29pbl93aXRoZHJhd191bnNpZ25lZCJ9" """ # Check transaction if self._transaction is None: raise ValueError("Transaction is none, build transaction first.") # Encode withdraw transaction raw if self._type == "bitcoin_withdraw_signed": return clean_transaction_raw( b64encode( str( json.dumps( dict( raw=self._transaction.hexlify(), fee=self._fee, network=self._network, type=self._type, ))).encode()).decode()) return clean_transaction_raw( b64encode( str( json.dumps( dict( fee=self._fee, raw=self._transaction.hexlify(), outputs=dict(value=self._htlc_utxo["value"], tx_output_n=0, script=self._htlc_utxo["script"]), network=self._network, type=self._type, ))).encode()).decode())
def sign(self, unsigned_raw, solver): """ Sign unsigned fund transaction raw. :param unsigned_raw: Bitcoin unsigned fund transaction raw. :type unsigned_raw: str :param solver: Bitcoin fund solver. :type solver: bitcoin.solver.FundSolver :returns: FundSignature -- Bitcoin fund signature instance. >>> from shuttle.providers.bitcoin.signature import FundSignature >>> from shuttle.providers.bitcoin.solver import FundSolver >>> bitcoin_fund_unsigned_raw = "eyJmZWUiOiA2NzgsICJyYXciOiAiMDIwMDAwMDAwMTg4OGJlN2VjMDY1MDk3ZDk1NjY0NzYzZjI3NmQ0MjU1NTJkNzM1ZmIxZDk3NGFlNzhiZjcyMTA2ZGNhMGYzOTEwMTAwMDAwMDAwZmZmZmZmZmYwMjEwMjcwMDAwMDAwMDAwMDAxN2E5MTQyYmIwMTNjM2U0YmViMDg0MjFkZWRjZjgxNWNiNjVhNWMzODgxNzhiODdiY2RkMGUwMDAwMDAwMDAwMTk3NmE5MTQ2NGE4MzkwYjBiMTY4NWZjYmYyZDRiNDU3MTE4ZGM4ZGE5MmQ1NTM0ODhhYzAwMDAwMDAwIiwgIm91dHB1dHMiOiBbeyJhbW91bnQiOiA5ODQ5NDYsICJuIjogMSwgInNjcmlwdCI6ICI3NmE5MTQ2NGE4MzkwYjBiMTY4NWZjYmYyZDRiNDU3MTE4ZGM4ZGE5MmQ1NTM0ODhhYyJ9XSwgIm5ldHdvcmsiOiAidGVzdG5ldCIsICJ0eXBlIjogImJpdGNvaW5fZnVuZF91bnNpZ25lZCJ9" >>> fund_solver = FundSolver("92cbbc5990cb5090326a76feeb321cad01048635afe5756523bbf9f7a75bf38b") >>> fund_signature = FundSignature(network="testnet") >>> fund_signature.sign(bitcoin_fund_unsigned_raw, fund_solver) <shuttle.providers.bitcoin.signature.FundSignature object at 0x0409DAF0> """ # Decoding and loading refund transaction fund_transaction = json.loads( b64decode( str(unsigned_raw + "=" * (-len(unsigned_raw) % 4)).encode()).decode()) # Checking refund transaction keys for key in ["raw", "outputs", "type", "fee", "network"]: if key not in fund_transaction: raise ValueError( "invalid Bitcoin unsigned fund transaction raw") if not fund_transaction["type"] == "bitcoin_fund_unsigned": raise TypeError( f"invalid Bitcoin fund unsigned transaction type, " f"you can't sign this {fund_transaction['type']} type by using FundSignature" ) if not isinstance(solver, FundSolver): raise TypeError( "invalid Bitcoin solver, it's only takes Bitcoin FundSolver class" ) # Setting transaction fee, type, network and transaction self._fee, self._type, self.network, self.transaction = ( fund_transaction["fee"], fund_transaction["type"], fund_transaction["network"], MutableTransaction.unhexlify(fund_transaction["raw"])) # Organizing outputs outputs = [] for output in fund_transaction["outputs"]: outputs.append( TxOut(value=output["amount"], n=output["n"], script_pubkey=Script.unhexlify( hex_string=output["script"]))) # Signing fund transaction self.transaction.spend(outputs, [solver.solve() for _ in outputs]) # Encoding fund transaction raw self._type = "bitcoin_fund_signed" self._signed_raw = b64encode( str( json.dumps( dict(raw=self.transaction.hexlify(), fee=fund_transaction["fee"], network=fund_transaction["network"], type=self._type))).encode()).decode() return self
def build_transaction( self, address: str, transaction_hash: str, locktime: int = config["locktime"]) -> "RefundTransaction": """ Build Bitcoin refund transaction. :param address: Bitcoin sender address. :type address: str :param transaction_hash: Bitcoin funded transaction hash/id. :type transaction_hash: str :param locktime: Bitcoin transaction lock time, defaults to ``0``. :type locktime: int :returns: RefundTransaction -- Bitcoin refund transaction instance. >>> from swap.providers.bitcoin.transaction import RefundTransaction >>> refund_transaction: RefundTransaction = RefundTransaction("testnet") >>> refund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") <swap.providers.bitcoin.transaction.RefundTransaction object at 0x0409DAF0> """ # Check parameter instances if not is_address(address, self._network): raise AddressError( f"Invalid Bitcoin sender '{address}' {self._network} address.") # Set address and transaction_hash self._address, self._transaction_hash, = address, transaction_hash # Get transaction self._transaction_detail = get_transaction( transaction_hash=self._transaction_hash, network=self._network) # Find HTLC UTXO self._htlc_utxo = find_p2sh_utxo(transaction=self._transaction_detail) if self._htlc_utxo is None: raise ValueError( "Invalid transaction id, there is no pay to script hash (P2SH) address." ) self._amount = self._htlc_utxo["value"] # Calculate the fee self._fee = fee_calculator(1, 1) outputs: list = [ TxOut(value=(self._amount - self._fee), n=0, script_pubkey=get_address_hash(address=self._address, script=True)) ] # Build mutable transaction self._transaction = MutableTransaction( version=self._version, ins=[ TxIn(txid=self._transaction_hash, txout=self._htlc_utxo["position"], script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=outputs, locktime=Locktime(locktime)) # Set transaction type self._type = "bitcoin_refund_unsigned" return self
class RefundTransaction(Transaction): """ Bitcoin Refund transaction. :param network: Bitcoin network, defaults to ``mainnet``. :type network: str :param version: Bitcoin transaction version, defaults to ``2``. :type version: int :returns: RefundTransaction -- Bitcoin refund transaction instance. .. warning:: Do not forget to build transaction after initialize refund transaction. """ def __init__(self, network: str = config["network"], version: int = config["version"]): super().__init__(network=network, version=version) self._transaction_hash: Optional[str] = None self._transaction_detail: Optional[dict] = None self._htlc_utxo: Optional[dict] = None self._interest: Optional[int] = None def build_transaction( self, address: str, transaction_hash: str, locktime: int = config["locktime"]) -> "RefundTransaction": """ Build Bitcoin refund transaction. :param address: Bitcoin sender address. :type address: str :param transaction_hash: Bitcoin funded transaction hash/id. :type transaction_hash: str :param locktime: Bitcoin transaction lock time, defaults to ``0``. :type locktime: int :returns: RefundTransaction -- Bitcoin refund transaction instance. >>> from swap.providers.bitcoin.transaction import RefundTransaction >>> refund_transaction: RefundTransaction = RefundTransaction("testnet") >>> refund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") <swap.providers.bitcoin.transaction.RefundTransaction object at 0x0409DAF0> """ # Check parameter instances if not is_address(address, self._network): raise AddressError( f"Invalid Bitcoin sender '{address}' {self._network} address.") # Set address and transaction_hash self._address, self._transaction_hash, = address, transaction_hash # Get transaction self._transaction_detail = get_transaction( transaction_hash=self._transaction_hash, network=self._network) # Find HTLC UTXO self._htlc_utxo = find_p2sh_utxo(transaction=self._transaction_detail) if self._htlc_utxo is None: raise ValueError( "Invalid transaction id, there is no pay to script hash (P2SH) address." ) self._amount = self._htlc_utxo["value"] # Calculate the fee self._fee = fee_calculator(1, 1) outputs: list = [ TxOut(value=(self._amount - self._fee), n=0, script_pubkey=get_address_hash(address=self._address, script=True)) ] # Build mutable transaction self._transaction = MutableTransaction( version=self._version, ins=[ TxIn(txid=self._transaction_hash, txout=self._htlc_utxo["position"], script_sig=ScriptSig.empty(), sequence=Sequence.max()) ], outs=outputs, locktime=Locktime(locktime)) # Set transaction type self._type = "bitcoin_refund_unsigned" return self def sign(self, solver: RefundSolver) -> "RefundTransaction": """ Sign Bitcoin refund transaction. :param solver: Bitcoin refund solver. :type solver: bitcoin.solver.RefundSolver :returns: RefundTransaction -- Bitcoin refund transaction instance. >>> from swap.providers.bitcoin.transaction import RefundTransaction >>> refund_transaction: RefundTransaction = RefundTransaction("testnet") >>> refund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") >>> bytecode: str = "63aa20821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e01588876a9140a0a6590e6ba4b48118d21b86812615219ece76b88ac67040ec4d660b17576a914e00ff2a640b7ce2d336860739169487a57f84b1588ac68" >>> refund_solver: RefundSolver = RefundSolver(xprivate_key="tprv8ZgxMBicQKsPeMHMJAc6uWGYiGqi1MVM2ybmzXL2TAoDpQe85uyDpdT7mv7Nhdu5rTCBEKLZsd9KyP2LQZJzZTvgVQvENArgU8e6DoYBiXf", bytecode=bytecode, endtime=1624687630) >>> refund_transaction.sign(solver=refund_solver) <swap.providers.bitcoin.transaction.RefundTransaction object at 0x0409DAF0> """ # Check parameter instances if not isinstance(solver, RefundSolver): raise TypeError( f"Solver must be Bitcoin RefundSolver, not {type(solver).__name__} type." ) if self._transaction is None: raise ValueError("Transaction is none, build transaction first.") self._transaction.spend([ TxOut(value=self._htlc_utxo["value"], n=0, script_pubkey=P2shScript.unhexlify( hex_string=self._htlc_utxo["script"])) ], [ P2shSolver( redeem_script=solver.witness(network=self._network), redeem_script_solver=solver.solve(network=self._network)) ]) # Set transaction type self._type = "bitcoin_refund_signed" return self def transaction_raw(self) -> str: """ Get Bitcoin refund transaction raw. :returns: str -- Bitcoin refund transaction raw. >>> from swap.providers.bitcoin.transaction import RefundTransaction >>> refund_transaction: RefundTransaction = RefundTransaction("testnet") >>> refund_transaction.build_transaction(address="n1wgm6kkzMcNfAtJmes8YhpvtDzdNhDY5a", transaction_hash="a211d21110756b266925fee2fbf2dc81529beef5e410311b38578dc3a076fb31") >>> refund_transaction.transaction_raw() "eyJmZWUiOiA1NzYsICJyYXciOiAiMDIwMDAwMDAwMTMxZmI3NmEwYzM4ZDU3MzgxYjMxMTBlNGY1ZWU5YjUyODFkY2YyZmJlMmZlMjU2OTI2NmI3NTEwMTFkMjExYTIwMDAwMDAwMDAwZmZmZmZmZmYwMTYwODQwMTAwMDAwMDAwMDAxOTc2YTkxNGUwMGZmMmE2NDBiN2NlMmQzMzY4NjA3MzkxNjk0ODdhNTdmODRiMTU4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IHsidmFsdWUiOiAxMDAwMDAsICJ0eF9vdXRwdXRfbiI6IDAsICJzY3JpcHQiOiAiYTkxNGM4Yzc3YTliNDNlZTJiZGYxYTA3YzQ4Njk5ODMzZDc2NjhiZjI2NGM4NyJ9LCAibmV0d29yayI6ICJ0ZXN0bmV0IiwgInR5cGUiOiAiYml0Y29pbl9yZWZ1bmRfdW5zaWduZWQifQ==" """ # Check transaction if self._transaction is None: raise ValueError("Transaction is none, build transaction first.") # Encode refund transaction raw if self._type == "bitcoin_refund_signed": return clean_transaction_raw( b64encode( str( json.dumps( dict( raw=self._transaction.hexlify(), fee=self._fee, network=self._network, type=self._type, ))).encode()).decode()) return clean_transaction_raw( b64encode( str( json.dumps( dict( fee=self._fee, raw=self._transaction.hexlify(), outputs=dict(value=self._htlc_utxo["value"], tx_output_n=0, script=self._htlc_utxo["script"]), network=self._network, type=self._type, ))).encode()).decode())
def sign(self, unsigned_raw, solver): """ Sign unsigned refund transaction raw. :param unsigned_raw: Bitcoin unsigned refund transaction raw. :type unsigned_raw: str :param solver: Bitcoin refund solver. :type solver: bitcoin.solver.RefundSolver :returns: RefundSignature -- Bitcoin refund signature instance. >>> from shuttle.providers.bitcoin.signature import RefundSignature >>> from shuttle.providers.bitcoin.solver import RefundSolver >>> bitcoin_refund_unsigned_raw = "eyJmZWUiOiA1NzYsICJyYXciOiAiMDIwMDAwMDAwMTUyYzIzZGM2NDU2N2IxY2ZhZjRkNzc2NjBjNzFjNzUxZjkwZTliYTVjMzc0N2ZhYzFkMDA1MTgwOGVhMGQ2NTEwMDAwMDAwMDAwZmZmZmZmZmYwMTQ4MTEwMDAwMDAwMDAwMDAxOTc2YTkxNDY0YTgzOTBiMGIxNjg1ZmNiZjJkNGI0NTcxMThkYzhkYTkyZDU1MzQ4OGFjMDAwMDAwMDAiLCAib3V0cHV0cyI6IHsidmFsdWUiOiA1MDAwLCAibiI6IDAsICJzY3JpcHRfcHVia2V5IjogImE5MTQ0MzNlOGVkNTliOWE2N2YwZjE4N2M2M2ViNDUwYjBkNTZlMjU2ZWMyODcifSwgIm5ldHdvcmsiOiAidGVzdG5ldCIsICJ0eXBlIjogImJpdGNvaW5fcmVmdW5kX3Vuc2lnbmVkIn0" >>> refund_solver = RefundSolver("92cbbc5990cb5090326a76feeb321cad01048635afe5756523bbf9f7a75bf38b", "3a26da82ead15a80533a02696656b14b5dbfd84eb14790f2e1be5e9e45820eeb", "muTnffLDR5LtFeLR2i3WsKVfdyvzfyPnVB", "mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q", 1000) >>> refund_signature = RefundSignature(network="testnet") >>> refund_signature.sign(unsigned_raw=bitcoin_refund_unsigned_raw, solver=refund_solver) <shuttle.providers.bitcoin.signature.RefundSignature object at 0x0409DAF0> """ # Decoding and loading refund transaction refund_transaction = json.loads( b64decode( str(unsigned_raw + "=" * (-len(unsigned_raw) % 4)).encode()).decode()) # Checking refund transaction keys for key in ["raw", "outputs", "type", "fee", "network"]: if key not in refund_transaction: raise ValueError( "invalid Bitcoin unsigned refund transaction raw") if not refund_transaction["type"] == "bitcoin_refund_unsigned": raise TypeError( f"invalid Bitcoin refund unsigned transaction type, " f"you can't sign this {refund_transaction['type']} type by using RefundSignature" ) if not isinstance(solver, RefundSolver): raise TypeError( "invalid Bitcoin solver, it's only takes Bitcoin RefundSolver class" ) # Setting transaction fee, type, network and transaction self._fee, self._type, self.network, self.transaction = ( refund_transaction["fee"], refund_transaction["type"], refund_transaction["network"], MutableTransaction.unhexlify(refund_transaction["raw"])) # Signing refund transaction self.transaction.spend([ TxOut( value=refund_transaction["outputs"]["value"], n=refund_transaction["outputs"]["n"], script_pubkey=P2shScript.unhexlify( hex_string=refund_transaction["outputs"]["script_pubkey"])) ], [ P2shSolver(redeem_script=solver.witness( network=refund_transaction["network"]), redeem_script_solver=solver.solve()) ]) # Encoding refund transaction raw self._type = "bitcoin_refund_signed" self._signed_raw = b64encode( str( json.dumps( dict(raw=self.transaction.hexlify(), fee=refund_transaction["fee"], network=refund_transaction["network"], type=self._type))).encode()).decode() return self
def sign_transaction(self, txins: Union[TxOut], tx: MutableTransaction) -> MutableTransaction: '''sign the parent txn outputs P2PKH''' solver = P2pkhSolver(self._private_key) return tx.spend(txins, [solver for i in txins])
def test_all(self): global keys priv = ExtendedPrivateKey.decode(keys[0][1]).key pk = priv.pub() addr_string = str(pk.to_address()) utxo = [] for i in range(3): # create 3 tx to add to UTXO txid = regtest.send_rpc_cmd(['sendtoaddress', addr_string, '100'], 0) to_spend = Transaction.unhexlify( regtest.send_rpc_cmd(['getrawtransaction', txid, '0'], 0)) txout = None for out in to_spend.outs: if str(out.script_pubkey.address()) == addr_string: txout = out break assert txout is not None utxo.append({ 'txid': txid, 'txout': txout, 'solver': P2pkhSolver(priv), 'next_seq': Sequence.max(), 'next_locktime': Locktime(0) }) regtest.send_rpc_cmd(['generate', '100'], 0) generate = False next_locktime = Locktime(0) next_sequence = Sequence.max() i = 0 while i < len(self.all) - 2: print('{:04d}\r'.format(i), end='', flush=True) ins = [ MutableTxIn(unspent['txid'], unspent['txout'].n, ScriptSig.empty(), unspent['next_seq']) for unspent in utxo ] outs = [] prev_types = [] for j, (unspent, script) in enumerate(zip(utxo, self.all[i:i + 3])): outs.append( TxOut(unspent['txout'].value - 1000000, j, script[0])) prev_types.append(script[2]) tx = MutableTransaction( 2, ins, outs, min_locktime(unspent['next_locktime'] for unspent in utxo)) mutable = copy.deepcopy(tx) tx = tx.spend([unspent['txout'] for unspent in utxo], [unspent['solver'] for unspent in utxo]) # print('====================') # print('txid: {}'.format(tx.txid)) # print() # print(tx) # print() # print('raw: {}'.format(tx.hexlify())) # print('prev_scripts, amounts, solvers:') print('TX: {}'.format(i)) regtest.send_rpc_cmd(['sendrawtransaction', tx.hexlify()], 0) print('Mempool size: {}'.format( len(regtest.send_rpc_cmd(['getrawmempool'], 0)))) if cmdline_args.dumpfile is not None: with open(cmdline_args.dumpfile, 'a') as out: for j, unspent in enumerate(utxo): json.dump( self.json_dump(unspent, tx.ins[j], j, copy.deepcopy(mutable).to_segwit()), out) out.write('\n') utxo = [] for j, (output, prev_type) in enumerate(zip(tx.outs, prev_types)): if 'time' in prev_type: if 'absolute' in prev_type: next_locktime = Locktime(100) next_sequence = Sequence(0xfffffffe) if 'relative' in prev_type: next_sequence = Sequence(3) generate = True else: next_locktime = Locktime(0) next_sequence = Sequence.max() utxo.append({ 'txid': tx.txid, 'txout': output, 'solver': self.all[i + j][1][0], # solver 'next_seq': next_sequence, 'next_locktime': next_locktime }) if generate: regtest.send_rpc_cmd(['generate', '4'], 0) generate = False if not i % 10: print('generating 2') regtest.send_rpc_cmd(['generate', '2'], 0) i += 1 ins = [ MutableTxIn(unspent['txid'], unspent['txout'].n, ScriptSig.empty(), unspent['next_seq']) for unspent in utxo ] tx = MutableTransaction( 2, ins, [ TxOut( sum(unspent['txout'].value for unspent in utxo) - 1000000, 0, self.final['script']) ], min_locktime(unspent['next_locktime'] for unspent in utxo)) tx = tx.spend([unspent['txout'] for unspent in utxo], [unspent['solver'] for unspent in utxo]) # print('====================') # print('txid: {}'.format(tx.txid)) # print() # print(tx) # print() # print('raw: {}'.format(tx.hexlify())) # print('prev_scripts, amounts, solvers:') # for unspent in utxo: # print(unspent['txout'].script_pubkey, unspent['txout'].value, unspent['solver'].__class__.__name__) regtest.send_rpc_cmd(['sendrawtransaction', tx.hexlify()], 0) regtest.teardown()
class FundTransaction(Transaction): """ Bitcoin FundTransaction class. :param version: Bitcoin transaction version, defaults to 2. :type version: int :param network: Bitcoin network, defaults to testnet. :type network: str :returns: FundTransaction -- Bitcoin fund transaction instance. .. warning:: Do not forget to build transaction after initialize fund transaction. """ # Initialization fund transaction def __init__(self, version=2, network="testnet"): super().__init__(version=version, network=network) # Initialization wallet, htlc, amount and unspent self.wallet, self.htlc, self.amount, self.unspent = None, None, None, None # Getting previous transaction indexes using funding amount self.previous_transaction_indexes = None # Building transaction def build_transaction(self, wallet, htlc, amount, locktime=0): """ Build Bitcoin fund transaction. :param wallet: Bitcoin sender wallet. :type wallet: bitcoin.wallet.Wallet :param htlc: Bitcoin hash time lock contract (HTLC). :type htlc: bitcoin.htlc.HTLC :param amount: Bitcoin amount to fund. :type amount: int :param locktime: Bitcoin transaction lock time, defaults to 0. :type locktime: int :returns: FundTransaction -- Bitcoin fund transaction instance. >>> from shuttle.providers.bitcoin.htlc import HTLC >>> from shuttle.providers.bitcoin.transaction import FundTransaction >>> from shuttle.providers.bitcoin.wallet import Wallet >>> htlc = HTLC(network="testnet").init("821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e0158", "muTnffLDR5LtFeLR2i3WsKVfdyvzfyPnVB", "mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q", 1000) >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett") >>> fund_transaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(wallet=sender_wallet, htlc=htlc, amount=10000) <shuttle.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Checking parameter instances if not isinstance(wallet, Wallet): raise TypeError( "invalid wallet instance, only takes Bitcoin Wallet class") if not isinstance(htlc, HTLC): raise TypeError( "invalid htlc instance, only takes Bitcoin HTLC class") if not isinstance(amount, int): raise TypeError("invalid amount instance, only takes integer type") # Setting wallet, htlc, amount and unspent self.wallet, self.htlc, self.amount = wallet, htlc, amount # Getting unspent transaction output self.unspent = self.wallet.unspent() # Setting previous transaction indexes self.previous_transaction_indexes = \ self.get_previous_transaction_indexes(amount=self.amount) # Getting transaction inputs and amount inputs, amount = self.inputs( utxos=self.unspent, previous_transaction_indexes=self.previous_transaction_indexes) # Calculating Bitcoin fee self._fee = fee_calculator(len(inputs), 2) if amount < (self.amount + self._fee): raise BalanceError("insufficient spend utxos") # Building mutable Bitcoin transaction self.transaction = MutableTransaction( version=self.version, ins=inputs, outs=[ # Funding into hash time lock contract script hash TxOut(value=self.amount, n=0, script_pubkey=P2shScript.unhexlify( hex_string=self.htlc.hash())), # Controlling amounts when we are funding on htlc script TxOut(value=(amount - (self._fee + self.amount)), n=1, script_pubkey=P2pkhScript.unhexlify( hex_string=self.wallet.p2pkh())) ], locktime=Locktime(locktime)) self._type = "bitcoin_fund_unsigned" return self # Signing fund transaction def sign(self, solver): """ Sign Bitcoin fund transaction. :param solver: Bitcoin fund solver. :type solver: bitcoin.solver.FundSolver :returns: FundTransaction -- Bitcoin fund transaction instance. >>> from shuttle.providers.bitcoin.htlc import HTLC >>> from shuttle.providers.bitcoin.transaction import FundTransaction >>> from shuttle.providers.bitcoin.solver import FundSolver >>> from shuttle.providers.bitcoin.wallet import Wallet >>> htlc = HTLC(network="testnet").init("821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e0158", "muTnffLDR5LtFeLR2i3WsKVfdyvzfyPnVB", "mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q", 1000) >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett") >>> fund_solver = FundSolver("92cbbc5990cb5090326a76feeb321cad01048635afe5756523bbf9f7a75bf38b") >>> fund_transaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(sender_wallet, htlc, 10000) >>> fund_transaction.sign(solver=fund_solver) <shuttle.providers.bitcoin.transaction.FundTransaction object at 0x0409DAF0> """ # Checking parameter instances if not isinstance(solver, FundSolver): raise TypeError( "invalid solver instance, only takes Bitcoin FundSolver class") if not self.unspent or not self.previous_transaction_indexes or not self.transaction: raise ValueError( "transaction script or unspent is none, build transaction first" ) # Organizing outputs outputs = self.outputs(self.unspent, self.previous_transaction_indexes) # Signing fund transaction self.transaction.spend(outputs, [solver.solve() for _ in outputs]) self._type = "bitcoin_fund_signed" return self # Automatically analysis previous transaction indexes using fund amount def get_previous_transaction_indexes(self, amount=None): if amount is None: amount = self.amount temp_amount = int() previous_transaction_indexes = list() for index, unspent in enumerate(self.unspent): temp_amount += unspent["amount"] if temp_amount > (amount + fee_calculator((index + 1), 2)): previous_transaction_indexes.append(index) break previous_transaction_indexes.append(index) return previous_transaction_indexes def unsigned_raw(self): """ Get Bitcoin unsigned fund transaction raw. :returns: str -- Bitcoin unsigned fund transaction raw. >>> from shuttle.providers.bitcoin.htlc import HTLC >>> from shuttle.providers.bitcoin.transaction import FundTransaction >>> from shuttle.providers.bitcoin.wallet import Wallet >>> htlc = HTLC(network="testnet").init("821124b554d13f247b1e5d10b84e44fb1296f18f38bbaa1bea34a12c843e0158", "muTnffLDR5LtFeLR2i3WsKVfdyvzfyPnVB", "mphBPZf15cRFcL5tUq6mCbE84XobZ1vg7Q", 1000) >>> sender_wallet = Wallet(network="testnet").from_passphrase("meherett") >>> fund_transaction = FundTransaction(network="testnet") >>> fund_transaction.build_transaction(sender_wallet, htlc, 10000) >>> fund_transaction.unsigned_raw() "eyJmZWUiOiA2NzgsICJyYXciOiAiMDIwMDAwMDAwMTJjMzkyMjE3NDgzOTA2ZjkwMmU3M2M0YmMxMzI4NjRkZTU4MTUzNzcyZDc5MjY4OTYwOTk4MTYyMjY2NjM0YmUwMTAwMDAwMDAwZmZmZmZmZmYwMmU4MDMwMDAwMDAwMDAwMDAxN2E5MTQ5NzE4OTRjNThkODU5ODFjMTZjMjA1OWQ0MjJiY2RlMGIxNTZkMDQ0ODdhNjI5MDAwMDAwMDAwMDAwMTk3NmE5MTQ2YmNlNjVlNThhNTBiOTc5ODk5MzBlOWE0ZmYxYWMxYTc3NTE1ZWYxODhhYzAwMDAwMDAwIiwgIm91dHB1dHMiOiBbeyJhbW91bnQiOiAxMjM0MCwgIm4iOiAxLCAic2NyaXB0IjogIjc2YTkxNDZiY2U2NWU1OGE1MGI5Nzk4OTkzMGU5YTRmZjFhYzFhNzc1MTVlZjE4OGFjIn1dLCAidHlwZSI6ICJiaXRjb2luX2Z1bmRfdW5zaWduZWQifQ" """ # Checking transaction and UTXO if not self.transaction or not self.unspent: raise ValueError( "transaction script or unspent is none, build transaction first" ) outputs = [] for index, utxo in enumerate(self.unspent): if self.previous_transaction_indexes is None or \ index in self.previous_transaction_indexes: outputs.append( dict(amount=utxo["amount"], n=utxo["output_index"], script=utxo["script"])) # Encoding fund transaction raw return b64encode( str( json.dumps( dict(fee=self._fee, raw=self.transaction.hexlify(), outputs=outputs, network=self.network, type="bitcoin_fund_unsigned"))).encode()).decode()