Created
January 31, 2024 20:49
-
-
Save ElliotFriend/c5d661c68f9206b96554808ecfebaed4 to your computer and use it in GitHub Desktop.
Stellar Escrow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 1. Alice creates the escrow account. | |
# 2. Alice updates the signers on the escrow account so both Alice and Bob are | |
# (multi-sig) signers, and adjusts the master key so she no longer has | |
# exclusive control of the escrow account. | |
# 3. Alice constructs a tx, whose source is the escrow, with timebounds. This tx | |
# will be executable in the future, and has Bob as the destination. This tx | |
# changes the permissions of the escrow account to give Bob full access at | |
# that time. | |
# 4. Alice signs it and sends the tx to Bob to review and sign. | |
# 5. Alice also generates a recovery tx with timebounds. This one says that | |
# after the date has passed, if Bob doesn't claim the money, Alice can take | |
# back control of the escrow account. You do this by using the same sequence | |
# number for both of the future txs, so only one can execute successfully. If | |
# Bob runs his tx, the recovery tx becomes unusable. | |
# 6. Alice signs the recovery tx and sends it to Bob to review and sign. | |
# 7. Bob signs the second tx and sends both back to Alice. Now both parties have | |
# a copy of both txs. | |
# 8. If both parties have signed both txs, then this implies an escrow | |
# agreement: you now have a construction that at the specified future date | |
# will allow the unlock of funds from the escrow account. | |
# 9. Finally, Alice funds the account. Once she's sent the money, she can't get | |
# it back until after the escrow period is up, and only if Bob doesn't claim | |
# it. | |
import requests | |
from stellar_sdk import Asset, TransactionBuilder, Server, Keypair, Network, Account | |
DEAL_SUCCEEDS: bool = True | |
# start with three keypairs (alice and bob, plus escrow) | |
alice_kp = Keypair.random() | |
bob_kp = Keypair.random() | |
escrow_kp = Keypair.random() | |
print(f"alice: {alice_kp.public_key}") | |
print(f" {alice_kp.secret}") | |
print(f"bob: {bob_kp.public_key}") | |
print(f" {bob_kp.secret}") | |
print(f"escrow: {escrow_kp.public_key}") | |
print(f" {escrow_kp.secret}\n") | |
# fund bob and alice, just to make things work | |
friendbot_url = "https://friendbot.stellar.org" | |
for kp in [alice_kp, bob_kp]: | |
resp = requests.get(friendbot_url, params={"addr": kp.public_key}) | |
server = Server('https://horizon-testnet.stellar.org') | |
alice_account = server.load_account(alice_kp) | |
setup_tx = ( | |
TransactionBuilder( | |
source_account=alice_account, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
# 1. Alice creates the escrow account | |
.append_begin_sponsoring_future_reserves_op( | |
sponsored_id=escrow_kp.public_key, | |
source=alice_kp.public_key, | |
) | |
.append_create_account_op( | |
destination=escrow_kp.public_key, | |
starting_balance=0.0, | |
) | |
# 2. Alice updates the signers on the escrow account so both Alice and Bob | |
# are (multi-sig) signers, and adjusts the master key so she no longer | |
# has exclusive control of the escrow account. | |
.append_ed25519_public_key_signer( | |
account_id=bob_kp.public_key, | |
weight=1, | |
source=escrow_kp.public_key, | |
) | |
.append_ed25519_public_key_signer( | |
account_id=alice_kp.public_key, | |
weight=1, | |
source=escrow_kp.public_key, | |
) | |
.append_set_options_op( | |
master_weight=0, | |
low_threshold=2, | |
med_threshold=2, | |
high_threshold=2, | |
source=escrow_kp.public_key | |
) | |
.append_end_sponsoring_future_reserves_op( | |
source=escrow_kp.public_key | |
) | |
.set_timeout(30) | |
.build() | |
) | |
setup_tx.sign(alice_kp) | |
setup_tx.sign(escrow_kp) | |
resp = server.submit_transaction(setup_tx) | |
escrow_account = server.load_account(escrow_kp) | |
# 3. Alice constructs a tx, whose source is the escrow, with timebounds. This tx | |
# will be executable in the future, and has Bob as the destination. This tx | |
# changes the permissions of the escrow account to give Bob full access at | |
# that time. | |
deal_success_tx = ( | |
TransactionBuilder( | |
source_account=Account( | |
account=escrow_account.account, | |
sequence=escrow_account.sequence + 1 | |
), | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
# min_time is in about 45 minutes, max_time is a year in the future | |
.add_time_bounds( | |
min_time=1706724000, | |
max_time=1738343631 | |
) | |
# a. remove alice as signer | |
.append_ed25519_public_key_signer( | |
account_id=alice_kp.public_key, | |
weight=0, | |
source=escrow_kp.public_key, | |
) | |
# b. adjust the thresholds so bob can do something with the account | |
.append_set_options_op( | |
low_threshold=1, | |
med_threshold=1, | |
high_threshold=1, | |
source=escrow_kp.public_key, | |
) | |
.build() | |
) | |
# 4. Alice signs it and sends the tx to Bob to review and sign. | |
# deal_success_tx.sign(alice_kp) # bob reviews/signs later | |
print(f"deal_success_tx: {deal_success_tx.to_xdr()}") | |
print(f"deal_success_tx.hash: {deal_success_tx.hash_hex()}\n") | |
# 5. Alice also generates a recovery tx with timebounds. This one says that | |
# after the date has passed, if Bob doesn't claim the money, Alice can take | |
# back control of the escrow account. You do this by using the same sequence | |
# number for both of the future txs, so only one can execute successfully. If | |
# Bob runs his tx, the recovery tx becomes unusable. | |
deal_fell_apart_tx = ( | |
TransactionBuilder( | |
source_account=Account( | |
account=escrow_account.account, | |
sequence=escrow_account.sequence + 1 | |
), | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
# min_time is in about 45 minutes, max_time is a year in the future | |
.add_time_bounds( | |
min_time=1706724000, | |
max_time=1738343631 | |
) | |
# a. remove bob as signer | |
.append_ed25519_public_key_signer( | |
account_id=bob_kp.public_key, | |
weight=0, | |
source=escrow_kp.public_key, | |
) | |
# b. adjust the thresholds so bob can do something with the account | |
.append_set_options_op( | |
low_threshold=1, | |
med_threshold=1, | |
high_threshold=1, | |
source=escrow_kp.public_key, | |
) | |
.build() | |
) | |
# 6. Alice signs the recovery tx and sends it to Bob to review and sign. | |
# deal_fell_apart_tx.sign(alice_kp) | |
print(f"deal_fell_apart_tx: {deal_fell_apart_tx.to_xdr()}") | |
print(f"deal_fell_apart_tx.hash: {deal_fell_apart_tx.hash_hex()}\n") | |
# 7. Bob signs the (first and) second tx and sends both back to Alice. Now both | |
# parties have a copy of both txs. | |
# deal_success_tx.sign(bob_kp) | |
# deal_fell_apart_tx.sign(bob_kp) | |
# 8. If both parties have signed both txs, then this implies an escrow | |
# agreement: you now have a construction that at the specified future date | |
# will allow the unlock of funds from the escrow account. | |
# | |
# Now these two transactions need to be added to the escrow account as pre-auth | |
# txs: | |
# - build the inner tx | |
pre_auth_signers_inner_tx = ( | |
TransactionBuilder( | |
source_account=escrow_account, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
.append_begin_sponsoring_future_reserves_op( | |
sponsored_id=escrow_kp.public_key, | |
source=alice_kp.public_key, | |
) | |
.append_pre_auth_tx_signer( | |
pre_auth_tx_hash=deal_success_tx.hash(), | |
weight=2, | |
) | |
.append_pre_auth_tx_signer( | |
pre_auth_tx_hash=deal_fell_apart_tx.hash(), | |
weight=2, | |
) | |
.append_end_sponsoring_future_reserves_op( | |
source=escrow_kp.public_key, | |
) | |
.set_timeout(30) | |
.build() | |
) | |
# - sign the inner tx (both alice and bob) | |
pre_auth_signers_inner_tx.sign(alice_kp) | |
pre_auth_signers_inner_tx.sign(bob_kp) | |
# - submit as a fee bump tx to the network | |
pre_auth_signers_fb_tx = ( | |
TransactionBuilder.build_fee_bump_transaction( | |
fee_source=alice_kp, | |
base_fee=10000, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
inner_transaction_envelope=pre_auth_signers_inner_tx | |
) | |
) | |
# - sign and submit to the network | |
pre_auth_signers_fb_tx.sign(alice_kp) | |
resp = server.submit_transaction(pre_auth_signers_fb_tx) | |
print(f"pre_auth txs added. tx.hash {pre_auth_signers_fb_tx.hash_hex()}") | |
# 9. Finally, Alice funds the account. Once she's sent the money, she can't get | |
# it back until after the escrow period is up, and only if Bob doesn't claim | |
# it. | |
funding_tx = ( | |
TransactionBuilder( | |
source_account=alice_account, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
.append_payment_op( | |
destination=escrow_kp.public_key, | |
asset=Asset.native(), | |
amount=9000.0 | |
) | |
.set_timeout(30) | |
.build() | |
) | |
funding_tx.sign(alice_kp) | |
resp = server.submit_transaction(funding_tx) | |
print(f"escrow account funded. tx.hash: {funding_tx.hash_hex()}\n") | |
### Time elapses and the deal either succeeds, or fell apart. | |
escrow_account.increment_sequence_number() | |
## Case 1: The deal was successfully completed, and the funds should be released | |
## to bob. | |
if DEAL_SUCCEEDS: | |
# bob submits the success transaction to the network | |
resp = server.submit_transaction(deal_success_tx) | |
print(f"success tx submitted") | |
# bob submits a transaction merging the escrow account into his own | |
claim_tx = ( | |
TransactionBuilder( | |
source_account=escrow_account, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
.append_pre_auth_tx_signer( | |
pre_auth_tx_hash=deal_fell_apart_tx.hash(), | |
weight=0, | |
) | |
.append_ed25519_public_key_signer( | |
account_id=bob_kp.public_key, | |
weight=0, | |
) | |
.append_account_merge_op( | |
destination=bob_kp.public_key, | |
) | |
.set_timeout(30) | |
.build() | |
) | |
claim_tx.sign(bob_kp) | |
resp = server.submit_transaction(claim_tx) | |
print(f"escrow account claimed by bob. tx.hash: {claim_tx.hash_hex()}\n") | |
## Case 2: The deal fell about, and the account should be recovered by alice. | |
else: | |
# alice submits the recovery transaction to the network | |
resp = server.submit_transaction(deal_fell_apart_tx) | |
print(f"recovery tx submitted") | |
escrow_account.increment_sequence_number() | |
# alice submits a transaction merging the escrow account into his own | |
recovery_tx = ( | |
TransactionBuilder( | |
source_account=escrow_account, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
.append_pre_auth_tx_signer( | |
pre_auth_tx_hash=deal_success_tx.hash(), | |
weight=0, | |
) | |
.append_ed25519_public_key_signer( | |
account_id=alice_kp.public_key, | |
weight=0, | |
) | |
.append_account_merge_op( | |
destination=alice_kp.public_key, | |
) | |
.set_timeout(30) | |
.build() | |
) | |
recovery_tx.sign(alice_kp) | |
resp = server.submit_transaction(recovery_tx) | |
print(f"escrow account recovered by alice. tx.hash: {recovery_tx.hash_hex()}\n") | |
# Epilogue: Return the funds to friendbot, just to be kind. | |
for kp in [alice_kp, bob_kp]: | |
account = server.load_account(kp) | |
merge_tx = ( | |
TransactionBuilder( | |
source_account=account, | |
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, | |
base_fee=10000, | |
) | |
.append_account_merge_op( | |
destination='GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR' | |
) | |
.set_timeout(30) | |
.build() | |
) | |
merge_tx.sign(kp) | |
resp = server.submit_transaction(merge_tx) | |
print(f"{kp.public_key} merged back into friendbot") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment