def test_sign_verify(self): unsigned = PSBT.from_string(B64PSBT) signed = PSBT.from_string(B64SIGNED) tx = unsigned.tx values = [inp.utxo.value for inp in unsigned.inputs] scripts = [inp.utxo.script_pubkey for inp in unsigned.inputs] for i, inp in enumerate(signed.inputs): wit = inp.final_scriptwitness.items[0] sig = SchnorrSig.parse(wit[:64]) if len(wit) == 65: sighash = wit[64] else: sighash = SIGHASH.DEFAULT hsh = tx.sighash_taproot(i, script_pubkeys=scripts, values=values, sighash=sighash) pub = PublicKey.from_xonly(inp.utxo.script_pubkey.data[2:]) self.assertTrue(pub.schnorr_verify(sig, hsh)) # check signing and derivation prv = ROOT.derive([0, i]) tweaked = prv.taproot_tweak(b"") self.assertEqual(pub.xonly(), tweaked.xonly()) sig2 = tweaked.schnorr_sign(hsh) self.assertTrue(pub.schnorr_verify(sig2, hsh)) self.assertEqual(sig, sig2) # sign with individual pks sigcount = unsigned.sign_with(prv.key, SIGHASH.ALL) self.assertEqual(sigcount, 1) self.assertEqual(unsigned.inputs[i].final_scriptwitness, signed.inputs[i].final_scriptwitness) for i, inp in enumerate(unsigned.inputs): prv = ROOT.derive([0, i]) # remove final scriptwitness to test signing with root inp.final_scriptwitness = None # populate derivation inp.bip32_derivations[prv.key.get_public_key()] = DerivationPath( ROOT.my_fingerprint, [0, i]) # test signing with root key counter = unsigned.sign_with(ROOT, SIGHASH.ALL) self.assertEqual(counter, 2) for inp1, inp2 in zip(unsigned.inputs, signed.inputs): self.assertEqual(inp1.final_scriptwitness, inp2.final_scriptwitness) inp1.final_scriptwitness = None # test signing with psbtview, unsigned already has derivations stream = BytesIO(unsigned.serialize()) psbtv = PSBTView.view(stream, compress=False) sigs = BytesIO() sigcount = psbtv.sign_with(ROOT, sigs, SIGHASH.ALL) self.assertEqual(sigcount, len(unsigned.inputs)) v = sigs.getvalue() # check sigs are in the stream for inp in signed.inputs: self.assertTrue(inp.final_scriptwitness.items[0] in v)
def test_invalid_psbts(self): for psbt_str in INVALID_VECTORS: psbt_bytes = unhexlify(psbt_str) try: PSBT.parse(psbt_bytes) except: continue # succeed if exception was thrown self.fail( "Invalid psbt was parsed successfully: {}".format(psbt_str))
def test_sign(self): """Test if we can sign psbtview and get the same as from signing psbt""" for compress in [True, False]: for b64 in PSBTS: psbt = PSBT.from_string(b64, compress=compress) stream = BytesIO(a2b_base64(b64)) psbtv = PSBTView.view(stream, compress=compress) # incomplete psbtview xpsbt = PSBT.from_string(b64, compress=compress) # remove derivations and other important data from original for sc in xpsbt.inputs: sc.bip32_derivations = {} sc.witness_script = None sc.redeem_script = None xstream = BytesIO(xpsbt.serialize()) xpsbtv = PSBTView.view(xstream, compress=compress) csigs1 = psbt.sign_with(ROOT) sigs_stream2 = BytesIO() csigs2 = psbtv.sign_with(ROOT, sigs_stream2) # signing incomplete psbtview sigs_stream3 = BytesIO() csigs3 = 0 for i in range(xpsbtv.num_inputs): csigs3 += xpsbtv.sign_input( i, ROOT, sigs_stream3, extra_scope_data=psbt.inputs[i]) # add separator sigs_stream3.write(b"\x00") self.assertEqual(csigs1, csigs2) self.assertEqual(csigs1, csigs3) for sigs_stream in [sigs_stream2, sigs_stream3]: sigs_stream.seek(0) # check all sigs signed_inputs = [ InputScope.read_from(sigs_stream) for i in range(len(psbt.inputs)) ] self.assertEqual(len(signed_inputs), len(psbt.inputs)) for i, inp in enumerate(signed_inputs): inp2 = psbt.inputs[i] self.assertEqual(inp.partial_sigs, inp2.partial_sigs) # check serialization with signatures sigs_stream.seek(0) ser = BytesIO() psbtv.write_to(ser, extra_input_streams=[sigs_stream2]) self.assertEqual(ser.getvalue(), psbt.serialize()) # check compress reduces the size of psbt sigs_stream2.seek(0) ser2 = BytesIO() psbtv.write_to(ser2, extra_input_streams=[sigs_stream2], compress=True) self.assertTrue(len(ser2.getvalue()) < len(ser.getvalue()))
def create_psbts(self, base64_psbt, wallet): psbts = super().create_psbts(base64_psbt, wallet) # remove non-witness utxo if they are there to reduce QR code size updated_psbt = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=False) qr_psbt = PSBT.parse(a2b_base64(updated_psbt)) # find my key fgp = None derivation = None for k in wallet.keys: if k in self.keys and k.fingerprint and k.derivation: fgp = bytes.fromhex(k.fingerprint) derivation = bip32.parse_path(k.derivation) break # remove unnecessary derivations from inputs and outputs for inp in qr_psbt.inputs + qr_psbt.outputs: # keep only my derivation for k in list(inp.bip32_derivations.keys()): if inp.bip32_derivations[k].fingerprint != fgp: inp.bip32_derivations.pop(k, None) # remove scripts from outputs (DIY should know about the wallet) for out in qr_psbt.outputs: out.witness_script = None out.redeem_script = None # remove partial sigs from inputs for inp in qr_psbt.inputs: inp.partial_sigs = {} psbts["qrcode"] = b2a_base64(qr_psbt.serialize()).strip().decode() # we can add xpubs to SD card, but non_witness can be too large for MCU psbts["sdcard"] = wallet.fill_psbt(psbts["qrcode"], non_witness=False, xpubs=True) return psbts
def replace_derivations(self, wallet, psbts): # cc wants everyone to use the same derivation fgp = None derivation = None for k in wallet.keys: if k in self.keys and k.fingerprint and k.derivation: fgp = k.fingerprint derivation = k.derivation break if not fgp: return path = bip32.parse_path(derivation) for kk in list(psbts.keys()): psbt = PSBT.parse(a2b_base64(psbts[kk])) for xpub in psbt.xpubs: psbt.xpubs[xpub].derivation = list(path) # remove partial signatures from device psbt for scope in psbt.inputs: scope.partial_sigs = OrderedDict() for scope in psbt.inputs + psbt.outputs: for k in list(scope.bip32_derivations.keys()): original = scope.bip32_derivations[k].derivation scope.bip32_derivations[ k].derivation = path + original[-2:] psbts[kk] = b2a_base64(psbt.serialize()).decode().strip()
def test_sign(self): """Parses a PSBT, signs both inputs (1 segwit and 1 legacy), and verifies each signature is correct""" xkey = HDKey.from_base58( "tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF" ) psbt_str = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000" exp_partial_sigs = [ { "029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f": "3044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01", "02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7": "3045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01", }, { "03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc": "3044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01", "023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73": "3044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d201", }, ] psbt = PSBT.parse(unhexlify(psbt_str)) psbt.sign_with(xkey) for i in range(len(psbt.inputs)): inp = psbt.inputs[i] self.assertEqual(len(inp.partial_sigs), len(exp_partial_sigs[i])) for act_pub, act_sig in inp.partial_sigs.items(): act_pub_str = hexlify(act_pub.serialize()).decode("utf-8") self.assertIn(act_pub_str, exp_partial_sigs[i].keys()) self.assertEqual( hexlify(act_sig).decode("utf-8"), exp_partial_sigs[i][act_pub_str])
def test_valid_psbts(self): for psbt_str in VALID_VECTORS: psbt_bytes = unhexlify(psbt_str) psbt_act = PSBT.parse(psbt_bytes) msg = "Valid psbt changed after being parsed & serialized: {}".format( psbt_str) self.assertEqual(psbt_act.serialize(), psbt_bytes, msg)
def convert_rawtransaction_to_psbt(wallet_rpc, rawtransaction) -> str: """ Converts a raw transaction in HEX format into a PSBT in b64 format """ tx = Transaction.from_string(rawtransaction) psbt = PSBT(tx) # this empties the signatures psbt = wallet_rpc.walletprocesspsbt(str(psbt), False).get("psbt", str(psbt)) psbt = PSBT.from_string(psbt) # we need the class object again # Recover signatures (witness or scriptsig) if available in raw tx for vin, psbtin in zip(tx.vin, psbt.inputs): if vin.witness: psbtin.final_scriptwitness = vin.witness if vin.script_sig: psbtin.final_scriptsig = vin.script_sig b64_psbt = str(psbt) return b64_psbt
def sign_with_descriptor(self, wname, d1, d2, all_sighashes=False): # to derive addresses desc1 = Descriptor.from_string(d1) desc2 = Descriptor.from_string(d2) # recv addr 2 addr1 = desc1.derive(2).address(NETWORKS['regtest']) # change addr 3 addr2 = desc2.derive(3).address(NETWORKS['regtest']) res = sim.query(f"bitcoin:{addr1}?index=2", [True]) # check it's found self.assertFalse(b"Can't find wallet" in res) # to add checksums d1 = rpc.getdescriptorinfo(d1)["descriptor"] d2 = rpc.getdescriptorinfo(d2)["descriptor"] rpc.createwallet(wname, True, True) w = rpc.wallet(wname) res = w.importmulti([{ "desc": d1, "internal": False, "timestamp": "now", "watchonly": True, "range": 10, },{ "desc": d2, "internal": True, "timestamp": "now", "watchonly": True, "range": 10, }],{"rescan": False}) self.assertTrue(all([k["success"] for k in res])) wdefault.sendtoaddress(addr1, 0.1) rpc.mine() psbt = w.walletcreatefundedpsbt([], [{wdefault.getnewaddress(): 0.002}], 0, {"includeWatching": True, "changeAddress": addr2}, True) unsigned = psbt["psbt"] sighashes = [None] if all_sighashes: sh = [SIGHASH.ALL, SIGHASH.NONE, SIGHASH.SINGLE] sighashes += sh + [s | SIGHASH.ANYONECANPAY for s in sh] tx = PSBT.from_base64(unsigned) for sh in sighashes: for inp in tx.inputs: inp.sighash_type = sh unsigned = tx.to_base64().encode() # confirm signing if sh in [SIGHASH.ALL, None]: signed = sim.query(b"sign "+unsigned, [True]) else: # confirm warning signed = sim.query(b"sign "+unsigned, [True, True]) # signed tx self.assertTrue(signed.startswith(b"cHNi")) combined = rpc.combinepsbt([unsigned.decode(), signed.decode()]) final = rpc.finalizepsbt(combined) self.assertTrue(final["complete"]) # broadcast res = rpc.testmempoolaccept([final["hex"]]) self.assertTrue(res[0]["allowed"])
def clean_psbt(b64psbt): try: psbt = PSBT.from_string(b64psbt) except: psbt = PSET.from_string(b64psbt) for inp in psbt.inputs: if inp.witness_utxo is not None and inp.non_witness_utxo is not None: inp.non_witness_utxo = None return psbt.to_string()
def sign_with_descriptor(self, d1, d2, root): rpc = daemon.rpc wname = random_wallet_name() # to derive addresses desc1 = Descriptor.from_string(d1) desc2 = Descriptor.from_string(d2) # recv addr 2 addr1 = desc1.derive(2).address(net) # change addr 3 addr2 = desc2.derive(3).address(net) # to add checksums d1 = add_checksum(str(d1)) d2 = add_checksum(str(d2)) rpc.createwallet(wname, True, True) w = daemon.wallet(wname) res = w.importmulti([{ "desc": d1, "internal": False, "timestamp": "now", "watchonly": True, "range": 10, }, { "desc": d2, "internal": True, "timestamp": "now", "watchonly": True, "range": 10, }], {"rescan": False}) self.assertTrue(all([k["success"] for k in res])) wdefault = daemon.wallet() wdefault.sendtoaddress(addr1, 0.1) daemon.mine() psbt = w.walletcreatefundedpsbt([], [{ wdefault.getnewaddress(): 0.002 }], 0, { "includeWatching": True, "changeAddress": addr2 }, True) unsigned = psbt["psbt"] psbt = PSBT.from_string(unsigned) psbt.sign_with(root) final = rpc.finalizepsbt(str(psbt)) self.assertTrue(final["complete"]) # test accept res = rpc.testmempoolaccept([final["hex"]]) self.assertTrue(res[0]["allowed"])
def create_psbts(self, base64_psbt, wallet): try: # remove rangeproofs and add sighash alls psbt = PSET.from_string(base64_psbt) for out in psbt.outputs: out.range_proof = None out.surjection_proof = None for inp in psbt.inputs: if not inp.sighash_type: inp.sighash_type = LSIGHASH.ALL base64_psbt = psbt.to_string() except: pass psbts = super().create_psbts(base64_psbt, wallet) # remove non-witness utxo if they are there to reduce QR code size updated_psbt = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=False) try: qr_psbt = PSBT.from_string(updated_psbt) except: qr_psbt = PSET.from_string(updated_psbt) # find my key fgp = None derivation = None for k in wallet.keys: if k in self.keys and k.fingerprint and k.derivation: fgp = bytes.fromhex(k.fingerprint) derivation = bip32.parse_path(k.derivation) break # remove unnecessary derivations from inputs and outputs for inp in qr_psbt.inputs + qr_psbt.outputs: # keep only my derivation for k in list(inp.bip32_derivations.keys()): if fgp and inp.bip32_derivations[k].fingerprint != fgp: inp.bip32_derivations.pop(k, None) # remove scripts from outputs (DIY should know about the wallet) for out in qr_psbt.outputs: out.witness_script = None out.redeem_script = None # remove partial sigs from inputs for inp in qr_psbt.inputs: inp.partial_sigs = {} psbts["qrcode"] = qr_psbt.to_string() # we can add xpubs to SD card, but non_witness can be too large for MCU psbts["sdcard"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True) return psbts
def test_scopes(self): """Tests that PSBT and PSBTView result in the same scopes and other constants""" for compress in [True, False]: for b64 in PSBTS: psbt = PSBT.from_string(b64, compress=compress) stream = BytesIO(a2b_base64(b64)) psbtv = PSBTView.view(stream, compress=compress) self.assertEqual(len(psbt.inputs), psbtv.num_inputs) self.assertEqual(len(psbt.outputs), psbtv.num_outputs) self.assertEqual(psbt.version, psbtv.version) if psbt.version != 2: self.assertTrue(psbtv.tx_offset > 0) # check something left when seek to n-1 psbtv.seek_to_scope(psbtv.num_inputs + psbtv.num_outputs - 1) self.assertEqual(len(stream.read(1)), 1) # check nothing left in the stream when seeking to the end psbtv.seek_to_scope(psbtv.num_inputs + psbtv.num_outputs) self.assertEqual(stream.read(1), b"") # check that all scopes are the same in psbt and psbtv # check random input scope first idx = len(psbt.inputs) // 2 self.assertEqual(psbt.inputs[idx].serialize(), psbtv.input(idx).serialize()) # check input scopes sequentially for i, inp in enumerate(psbt.inputs): self.assertEqual(inp.serialize(), psbtv.input(i).serialize()) self.assertEqual(inp.vin.serialize(), psbtv.vin(i).serialize()) # check random output scope idx = len(psbt.outputs) // 2 self.assertEqual(psbt.outputs[idx].serialize(), psbtv.output(idx).serialize()) # check input scopes sequentially for i, out in enumerate(psbt.outputs): self.assertEqual(out.serialize(), psbtv.output(i).serialize()) self.assertEqual(out.vout.serialize(), psbtv.vout(i).serialize()) self.assertEqual(psbt.tx_version, psbtv.tx_version) self.assertEqual(psbt.locktime, psbtv.locktime)
def test_miniscript(self): # and(pk(A),after(100)) -> and_v(v:pk(A),after(100)) path = "49h/1h/0h/2h" fgp = sim.query("fingerprint").decode() xpub = sim.query(f"xpub m/{path}").decode() desc = f"wsh(and_v(v:pk([{fgp}/{path}]{xpub}"+"/{0,1}/*),after(10)))" res = sim.query("addwallet mini&"+desc, [True]) wname = wallet_prefix+"_mini" d = Descriptor.from_string(desc) addr = d.derive(5).address(NETWORKS['regtest']) # check it finds the wallet correctly sc = d.derive(5).witness_script().data.hex() res = sim.query(f"showaddr wsh m/{path}/0/5 {sc}", [True]) self.assertEqual(res.decode(), addr) d1 = d.derive(2, branch_index=0) d2 = d.derive(3, branch_index=1) # recv addr 2 addr1 = d1.address(NETWORKS['regtest']) # change addr 3 addr2 = d2.address(NETWORKS['regtest']) res = sim.query(f"bitcoin:{addr1}?index=2", [True]) # check it's found self.assertFalse(b"Can't find wallet" in res) rpc.createwallet(wname, True, True) w = rpc.wallet(wname) res = w.importmulti([{ "scriptPubKey": {"address": addr1},#d1.script_pubkey().data.hex(), # "witnessscript": d1.witness_script().data.hex(), # "pubkeys": [k.sec().hex() for k in d1.keys], "internal": False, "timestamp": "now", "watchonly": True, },{ "scriptPubKey": {"address": addr2},#d2.script_pubkey().data.hex(), # "witnessscript": d2.witness_script().data.hex(), # "pubkeys": [k.sec().hex() for k in d2.keys], "internal": True, "timestamp": "now", "watchonly": True, }],{"rescan": False}) self.assertTrue(all([k["success"] for k in res])) wdefault.sendtoaddress(addr1, 0.1) rpc.mine() unspent = w.listunspent() self.assertTrue(len(unspent) > 0) unspent = [{"txid": u["txid"], "vout": u["vout"]} for u in unspent[:1]] tx = w.createrawtransaction(unspent, [{wdefault.getnewaddress(): 0.002},{addr2: 0.09799}]) psbt = PSBT.from_base64(w.converttopsbt(tx)) # locktime magic :) psbt.tx.locktime = 11 psbt.tx.vin[0].sequence = 10 # fillinig psbt with data psbt.inputs[0].witness_script = d1.witness_script() pub = ec.PublicKey.parse(d1.keys[0].sec()) psbt.inputs[0].bip32_derivations[pub] = DerivationPath(bytes.fromhex(fgp), bip32.parse_path(f"m/{path}")+[0,2]) tx = w.gettransaction(unspent[0]["txid"]) t = Transaction.from_string(tx["hex"]) psbt.inputs[0].witness_utxo = t.vout[unspent[0]["vout"]] psbt.outputs[1].witness_script = d2.witness_script() pub2 = ec.PublicKey.parse(d2.keys[0].sec()) psbt.outputs[1].bip32_derivations[pub2] = DerivationPath(bytes.fromhex(fgp), bip32.parse_path(f"m/{path}")+[1,3]) unsigned = psbt.to_base64() # confirm signing signed = sim.query("sign "+unsigned, [True]) stx = PSBT.from_base64(signed.decode()) # signed tx t = psbt.tx # print(stx) t.vin[0].witness = Witness([stx.inputs[0].partial_sigs[pub], psbt.inputs[0].witness_script.data]) # broadcast with self.assertRaises(Exception): res = rpc.sendrawtransaction(t.serialize().hex()) rpc.mine(11) res = rpc.sendrawtransaction(t.serialize().hex()) rpc.mine() self.assertEqual(len(bytes.fromhex(res)), 32)
def create_psbts(self, base64_psbt, wallet): # liquid transaction if base64_psbt.startswith("cHNl"): # remove rangeproofs and add sighash alls psbt = PSET.from_string(base64_psbt) # make sure we have tx blinding seed in the transaction if psbt.unknown.get(b"\xfc\x07specter\x00"): for out in psbt.outputs: out.range_proof = None # out.surjection_proof = None # we know assets - we can blind it if out.asset: out.asset_commitment = None out.asset_blinding_factor = None # we know value - we can blind it if out.value: out.value_commitment = None out.value_blinding_factor = None for inp in psbt.inputs: if inp.value and inp.asset: inp.range_proof = None else: psbt = PSBT.from_string(base64_psbt) fill_external_wallet_derivations(psbt, wallet) base64_psbt = psbt.to_string() psbts = super().create_psbts(base64_psbt, wallet) # remove non-witness utxo if they are there to reduce QR code size updated_psbt = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=False, taproot_derivations=True) try: qr_psbt = PSBT.from_string(updated_psbt) except: qr_psbt = PSET.from_string(updated_psbt) # find my key fgp = None derivation = None for k in wallet.keys: if k in self.keys and k.fingerprint and k.derivation: fgp = bytes.fromhex(k.fingerprint) derivation = bip32.parse_path(k.derivation) break # remove unnecessary derivations from inputs and outputs for inp in qr_psbt.inputs + qr_psbt.outputs: # keep only one derivation path (idealy ours) found = False pubkeys = list(inp.bip32_derivations.keys()) for i, pub in enumerate(pubkeys): if fgp and inp.bip32_derivations[pub].fingerprint != fgp: # only pop if we already saw our derivation # or if it's not the last one if found or i < len(pubkeys) - 1: inp.bip32_derivations.pop(k, None) else: found = True # remove scripts from outputs (DIY should know about the wallet) for out in qr_psbt.outputs: out.witness_script = None out.redeem_script = None # remove partial sigs from inputs for inp in qr_psbt.inputs: inp.partial_sigs = {} psbts["qrcode"] = qr_psbt.to_string() # we can add xpubs to SD card, but non_witness can be too large for MCU psbts["sdcard"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True, taproot_derivations=True) psbts["hwi"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True, taproot_derivations=True) return psbts
# round fee to satoshis fee = int(fee) + 1 vin = [ TransactionInput(bytes.fromhex(inp["txid"]), inp["vout"]) for inp in inputs ] vout = [TransactionOutput(AMOUNT, script.address_to_scriptpubkey(DESTINATION))] # add change output if spending_amount - fee - AMOUNT > DUST_LIMIT: vout.append( TransactionOutput(spending_amount - fee - AMOUNT, change_output.script_pubkey()), ) tx = Transaction(vin=vin, vout=vout) # now create PSBT from this transaction psbt = PSBT(tx) # fill missing information for all inputs and change output for i, inp in enumerate(inputs): psbt.inputs[i].bip32_derivations = inp["bip32_derivations"] psbt.inputs[i].witness_script = inp["witness_script"] psbt.inputs[i].redeem_script = inp["redeem_script"] psbt.inputs[i].witness_utxo = inp.get("witness_utxo", None) psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None) if len(psbt.outputs) > 1: # derivation information for utxos bip32_derivations = {} for k in change_output.keys: bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath( k.origin.fingerprint, k.origin.derivation)