Skip to content

Instantly share code, notes, and snippets.

@ElliotFriend
Created January 31, 2024 20:49
Show Gist options
  • Save ElliotFriend/c5d661c68f9206b96554808ecfebaed4 to your computer and use it in GitHub Desktop.
Save ElliotFriend/c5d661c68f9206b96554808ecfebaed4 to your computer and use it in GitHub Desktop.
Stellar Escrow
# 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