Skip to content

Instantly share code, notes, and snippets.

@kdmukai
Last active January 24, 2024 15:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kdmukai/7f4572488c29618c3e66ee4db0f74083 to your computer and use it in GitHub Desktop.
Save kdmukai/7f4572488c29618c3e66ee4db0f74083 to your computer and use it in GitHub Desktop.
Extract and reuse outputs from a psbt using `embit`
from binascii import a2b_base64, unhexlify
from copy import deepcopy
from io import BytesIO
from embit.networks import NETWORKS
from embit.psbt import PSBT, OutputScope
# one input, one external recipient + change
psbt_base64 = "cHNidP8BAHECAAAAAU4T/0aX9mmNZHyKh+0AHYY+EtdJxndMRra0gn4QPCZdAQAAAAD9////Auj88QUAAAAAFgAUrME0Bwnpt7q+/5IYBvBXDGUr0FiQ0AMAAAAAABYAFGc8e3PH4CM45A3Z1h7cB0AEaAmTcAAAAE8BBDWHzwNXmUmVgAAAANRFa7R5gYD84Wbha3d1QnjgfYPOBw87on6cXS32WoyqAsPFtPxB7PRTdbujUnBPUVDh9YUBtwrl4nc0OcRNGvIyEA+4gv9UAACAAQAAgAAAAIAAAQCIAgAAAAHVNy3baqUJbmJM5kN9epW7oIqXB1O2s+Fs8julxND8ZQEAAAAXFgAUI+kCxhZQ0mdMSs6OSgKGdDKUanr9////As0uGh4BAAAAFgAUjiVTkQBkiXD8ylfqveCHXOprMQ4A4fUFAAAAABYAFFiMuj7Djc1P7mvOA8I27Lv2VmMObgAAAAEBHwDh9QUAAAAAFgAUWIy6PsONzU/ua84Dwjbsu/ZWYw4BAwQBAAAAIgYC9duqeSZYNc80SQfOc/SXZUUWqXZamBfjbIPdn18lj/cYD7iC/1QAAIABAACAAAAAgAAAAAADAAAAACICAiwV79CMgipih/G0K2ww7M7UfxxUhMPn1y52gKMFiT0nGA+4gv9UAACAAQAAgAAAAIABAAAAAAAAAAAA"
psbt = PSBT.parse(a2b_base64(psbt_base64))
print(psbt.outputs)
# >> [OutputScope(2202022c15efd08c822a6287f1b42b6c30ecced47f1c5484c3e7d72e7680a305893d27180fb882ff540000800100008000000080010000000000000000), OutputScope(00)]
# First output is our change, second is an external recipient
# Must specify `version=2` so that the script_pubkey is included
change_output_data = psbt.outputs[0].serialize(version=2).hex()
external_recipient_output_data = psbt.outputs[1].serialize(version=2).hex()
# Sanity checks; restoring from the hex data should yield the same output
orig_change_output = psbt.outputs[0]
change_output = OutputScope.read_from(BytesIO(unhexlify(change_output_data)))
orig_recipient_output = psbt.outputs[1]
recipient_output = OutputScope.read_from(BytesIO(unhexlify(external_recipient_output_data)))
assert orig_change_output == change_output
assert orig_change_output.script_pubkey == change_output.script_pubkey
assert orig_change_output.script_pubkey.address(NETWORKS["regtest"]) == change_output.script_pubkey.address(NETWORKS["regtest"])
assert orig_change_output.bip32_derivations == change_output.bip32_derivations
assert orig_change_output.value == orig_change_output.value
assert orig_recipient_output == recipient_output
assert orig_recipient_output.script_pubkey == recipient_output.script_pubkey
assert orig_recipient_output.script_pubkey.address(NETWORKS["regtest"]) == recipient_output.script_pubkey.address(NETWORKS["regtest"])
assert orig_recipient_output.bip32_derivations == recipient_output.bip32_derivations
assert orig_recipient_output.value == recipient_output.value
# Verify that outputs can just be deleted or appended to a psbt
new_psbt = deepcopy(psbt)
new_psbt.outputs.clear()
assert new_psbt.serialize() != psbt.serialize()
new_psbt.outputs.append(change_output)
new_psbt.outputs.append(recipient_output)
assert new_psbt.serialize() == psbt.serialize()
# Okay, but output order matters!
new_psbt.outputs.clear()
new_psbt.outputs.append(recipient_output)
new_psbt.outputs.append(change_output)
assert new_psbt.serialize() != psbt.serialize()
# Now start from a different PSBT (two inputs, one taproot recipient + change)
psbt2_base64 = "cHNidP8BAKYCAAAAAsJ89R3J4RM3F/2mTME/ag5R+BIhKutmLi6PRugo3Y95AAAAAAD9////ThP/Rpf2aY1kfIqH7QAdhj4S10nGd0xGtrSCfhA8Jl0BAAAAAP3///8CkNADAAAAAAAiUSBx39QNrpwz8trkdbIaRO9k4gWnqpMPcyUdlq/n8IOaW+jd5wsAAAAAFgAUrME0Bwnpt7q+/5IYBvBXDGUr0FhwAAAATwEENYfPA1eZSZWAAAAA1EVrtHmBgPzhZuFrd3VCeOB9g84HDzuifpxdLfZajKoCw8W0/EHs9FN1u6NScE9RUOH1hQG3CuXidzQ5xE0a8jIQD7iC/1QAAIABAACAAAAAgAABAHECAAAAAfFB2o8nAMMw9ZFG4EHYvsMle9RkAw0xCXoVr12ZRrRqAQAAAAD9////AgDh9QUAAAAAFgAUkafX/Pco8lEPjPg2Vyd2MC0Cv8w/ijgMAQAAABYAFLHv1XF6upFAIyRP61rF77j3NegLbwAAAAEBHwDh9QUAAAAAFgAUkafX/Pco8lEPjPg2Vyd2MC0Cv8wBAwQBAAAAIgYCKALMbvmaFrAPq1M4ikZhDSVahyZhdJAi3ScfXi9k/JsYD7iC/1QAAIABAACAAAAAgAAAAAAFAAAAAAEAiAIAAAAB1Tct22qlCW5iTOZDfXqVu6CKlwdTtrPhbPI7pcTQ/GUBAAAAFxYAFCPpAsYWUNJnTErOjkoChnQylGp6/f///wLNLhoeAQAAABYAFI4lU5EAZIlw/MpX6r3gh1zqazEOAOH1BQAAAAAWABRYjLo+w43NT+5rzgPCNuy79lZjDm4AAAABAR8A4fUFAAAAABYAFFiMuj7Djc1P7mvOA8I27Lv2VmMOAQMEAQAAACIGAvXbqnkmWDXPNEkHznP0l2VFFql2WpgX42yD3Z9fJY/3GA+4gv9UAACAAQAAgAAAAIAAAAAAAwAAAAAAIgICLBXv0IyCKmKH8bQrbDDsztR/HFSEw+fXLnaAowWJPScYD7iC/1QAAIABAACAAAAAgAEAAAAAAAAAAA=="
psbt2 = PSBT.parse(a2b_base64(psbt2_base64))
assert psbt.serialize() != psbt2.serialize()
# We can add the external recipient from the first PSBT as an additional output
psbt2.outputs.append(recipient_output)
# Just have to fix up the output amounts to make sense; reduce the change
# coming back.
psbt2.outputs[1].value -= psbt2.outputs[2].value
assert sum([output.value for output in psbt2.outputs]) == sum([inp.utxo.value for inp in psbt2.inputs]) - psbt2.fee()
# Can paste result into bip174.org and review
print(psbt2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment