diff --git a/_code-samples/sponsored-fees-and-reserves/README.md b/_code-samples/sponsored-fees-and-reserves/README.md new file mode 100644 index 00000000..212f0b92 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/README.md @@ -0,0 +1,3 @@ +# Sponsored Fees and Reserves Examples + +Shows how to sponsor transaction fees and reserves for other accounts on the XRP Ledger. diff --git a/_code-samples/sponsored-fees-and-reserves/py/README.md b/_code-samples/sponsored-fees-and-reserves/py/README.md new file mode 100644 index 00000000..33fa4af8 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/README.md @@ -0,0 +1,345 @@ +# Sponsored Fees and Reserves Examples (Python) + +This directory contains Python examples demonstrating how to sponsor transaction fees and reserves for other accounts on the XRP Ledger. + +## Setup + +Install dependencies before running any examples: + +```sh +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## Sponsor a Transaction (Co-signed) + +```sh +python sponsor_co_signed.py +``` + +The script should output the sponsored MPTokenAuthorize transaction and sponsorship details: + +```sh +=== Loading setup data... === + +MPT Issuance ID: 0000A98C748BDAE6F4A202E5B1EF4EE65A3EFA6C65EA9E65 +Issuer address: rBdNvZ3z1JuioZaXejFXQkf5otfnuwPNFD + +=== Creating wallets... === +Sponsor address: rJq1SZRgbPyi6inJjvU5Uoux1rJNaTaJyu +Funded sponsor with 100 XRP + +Sponsee address: rLu5q357e4obMEo68CCS3Le2fT4ahikMHv + +=== Creating sponsee's account... === +{ + "Account": "rJq1SZRgbPyi6inJjvU5Uoux1rJNaTaJyu", + "TransactionType": "Payment", + "Flags": 524288, + "SigningPubKey": "", + "Amount": "1", + "Destination": "rLu5q357e4obMEo68CCS3Le2fT4ahikMHv" +} + +Sponsee account created successfully! + +=== Preparing sponsored transaction... === +{ + "Account": "rLu5q357e4obMEo68CCS3Le2fT4ahikMHv", + "TransactionType": "MPTokenAuthorize", + "Fee": "10", + "Sequence": 79694, + "LastLedgerSequence": 79715, + "SigningPubKey": "EDAE01CC5520AA86F36F267115634B72EA7545C44B12FA20DB08EF42D9BDB97840", + "TxnSignature": "2F05FF9DF5F49621A9BF3950947BDF4ED1A19A6028B5E08CC96AEACE2131BEA680C9A533F50CD10EB35B27CD041BF070AC82AA214C3A99FB661509CAD767AC0A", + "Sponsor": "rJq1SZRgbPyi6inJjvU5Uoux1rJNaTaJyu", + "SponsorFlags": 3, + "SponsorSignature": { + "SigningPubKey": "ED325E2D6C23A4FB9D6985BC3500C9E7C645ED769A7BC121FEB4ACC20ADBE9C909", + "TxnSignature": "5E1FF4E17D6FEF32045691E5BA6480440AC9CCEF7D7F2F25B638673FB4A9E9AC4BE895B0823F41AD0DFC8FFBCB899070AE261D5D4072EE742222625C72036804" + }, + "MPTokenIssuanceID": "0000A98C748BDAE6F4A202E5B1EF4EE65A3EFA6C65EA9E65" +} + +=== Submitting sponsored transaction... === +Transaction successfully sponsored! +{ + "Account": "rLu5q357e4obMEo68CCS3Le2fT4ahikMHv", + "Fee": "10", + "LastLedgerSequence": 79715, + "MPTokenIssuanceID": "0000A98C748BDAE6F4A202E5B1EF4EE65A3EFA6C65EA9E65", + "Sequence": 79694, + "SigningPubKey": "EDAE01CC5520AA86F36F267115634B72EA7545C44B12FA20DB08EF42D9BDB97840", + "Sponsor": "rJq1SZRgbPyi6inJjvU5Uoux1rJNaTaJyu", + "SponsorFlags": 3, + "SponsorSignature": { + "SigningPubKey": "ED325E2D6C23A4FB9D6985BC3500C9E7C645ED769A7BC121FEB4ACC20ADBE9C909", + "TxnSignature": "5E1FF4E17D6FEF32045691E5BA6480440AC9CCEF7D7F2F25B638673FB4A9E9AC4BE895B0823F41AD0DFC8FFBCB899070AE261D5D4072EE742222625C72036804" + }, + "TransactionType": "MPTokenAuthorize", + "TxnSignature": "2F05FF9DF5F49621A9BF3950947BDF4ED1A19A6028B5E08CC96AEACE2131BEA680C9A533F50CD10EB35B27CD041BF070AC82AA214C3A99FB661509CAD767AC0A", + "ctid": "C001375000000066", + "date": 830292742, + "ledger_index": 79696 +} + +Sponsorship details -------------------------------------- + Sponsor: + Fee deducted: 10 drops + Balance: 99999979 drops + Reserves sponsored: 1 + + Sponsee: + Fee deducted: 0 drops + Balance: 1 drops +``` + +## Sponsor a Transaction (Pre-funded Pool) + +```sh +python sponsor_pre_funded.py +``` + +The script should output the sponsorship pool creation and the sponsored transaction details: + +```sh +=== Loading setup data... === + +MPT Issuance ID: 0000A98C748BDAE6F4A202E5B1EF4EE65A3EFA6C65EA9E65 +Issuer address: rBdNvZ3z1JuioZaXejFXQkf5otfnuwPNFD + +=== Creating wallets... === +Sponsor address: rwV12Ti6bzm8K7bqiLKSqJWajCJSziDTqB +Funded sponsor with 100 XRP + +Sponsee address: rNp5uQdjanayFWfrcHD7uEcyq7eiYADgZ4 + +=== Creating sponsee's account... === +{ + "Account": "rwV12Ti6bzm8K7bqiLKSqJWajCJSziDTqB", + "TransactionType": "Payment", + "Flags": 524288, + "SigningPubKey": "", + "Amount": "1", + "Destination": "rNp5uQdjanayFWfrcHD7uEcyq7eiYADgZ4" +} + +Sponsee account created successfully! + +=== Creating sponsorship pool... === +{ + "Account": "rwV12Ti6bzm8K7bqiLKSqJWajCJSziDTqB", + "TransactionType": "SponsorshipSet", + "SigningPubKey": "", + "Sponsee": "rNp5uQdjanayFWfrcHD7uEcyq7eiYADgZ4", + "FeeAmount": "1000000", + "ReserveCount": 5 +} + +Sponsorship pool created successfully! +Sponsorship ID: 214929E9019F4BC4D59C96E08229AB50FB5A771315C35FF9316A9E151751E4E4 + +=== Submitting sponsored transaction... === +Transaction successfully sponsored! +{ + "Account": "rNp5uQdjanayFWfrcHD7uEcyq7eiYADgZ4", + "Fee": "10", + "LastLedgerSequence": 79503, + "MPTokenIssuanceID": "0000A98C748BDAE6F4A202E5B1EF4EE65A3EFA6C65EA9E65", + "Sequence": 79480, + "SigningPubKey": "EDC46E55C48BF0CFD99EF75EAA80FA17DF0A9100D2A441E6A460FA9C876F0367F9", + "Sponsor": "rwV12Ti6bzm8K7bqiLKSqJWajCJSziDTqB", + "SponsorFlags": 3, + "TransactionType": "MPTokenAuthorize", + "TxnSignature": "E6526057E301FAB9BF071FA2A931AD82A96FBF6ADF23A3EBBD1EC39F065FDEE614CF777EB519A3019639AC87173C6D588BE20F99922F904FBE2F1EE845F28408", + "ctid": "C001367C00000066", + "date": 830292530, + "ledger_index": 79484 +} + +Sponsorship details -------------------------------------- + Pool: + Fee deducted: 10 drops + Balance: 999990 drops + Reserves consumed: 1 + + Sponsor: + Fee deducted: 0 drops + Balance: 98999979 drops + Reserves sponsored: 1 + + Sponsee: + Fee deducted: 0 drops + Balance: 1 drops +``` + +## Transfer a Reserve Sponsorship + +```sh +python transfer_sponsorship.py +``` + +The script should output the full lifecycle of a reserve sponsorship — create, reassign, and end: + +```sh +=== Loading setup data... === +MPT Issuance ID: 0000A98C748BDAE6F4A202E5B1EF4EE65A3EFA6C65EA9E65 +Issuer address: rBdNvZ3z1JuioZaXejFXQkf5otfnuwPNFD + +=== Funding accounts... === +Sponsor A address: rM8C8CsPpnwno5NZ5XqU6gknvkzveUku21 +Sponsor B address: rD8djknRvCyqGi4GTZ1826pxJsNTe8Sw1g +Sponsee address: rhpYcHHetQdCnMfKn4dvnAtR4fpKSTMyjU + +=== Submitting unsponsored MPTokenAuthorize transaction... === +MPTokenAuthorize transaction successful! +MPToken ID: C7E9F77AE8D414E4537715D4C58C9BA5B524D2E3F84A8C47FC70BA7EF6F0B001 + +=== Creating reserve sponsorship... === +Submitting SponsorshipTransfer transaction... +Sponsor A (rM8C8CsPpnwno5NZ5XqU6gknvkzveUku21) is now sponsoring the MPToken reserve! +{ + "Account": "rhpYcHHetQdCnMfKn4dvnAtR4fpKSTMyjU", + "Fee": "10", + "Flags": 2, + "LastLedgerSequence": 79633, + "ObjectID": "C7E9F77AE8D414E4537715D4C58C9BA5B524D2E3F84A8C47FC70BA7EF6F0B001", + "Sequence": 79611, + "SigningPubKey": "ED5DD35377B24FFF8AEEDDC3BA9B68E89FB1BEDAC91524322D1066A9BC0EFFCCA5", + "Sponsor": "rM8C8CsPpnwno5NZ5XqU6gknvkzveUku21", + "SponsorFlags": 2, + "SponsorSignature": { + "SigningPubKey": "EDF6368FFA10FCC71DF320CC2E605B85D74614C6255060D567A382719A28590B0C", + "TxnSignature": "34A861A42F95EA4898315490737233BE63725C0CDCCD2C70202B3B0D3F3F03402A2BB623E458907195A926E3F4C7C7DA6BCA3CAECB3484FB72B180F388AFB10B" + }, + "TransactionType": "SponsorshipTransfer", + "TxnSignature": "4C48840DA1F82D5B8C2820AB0E2E0B319792AB3D7B4212DB5571C0FDB815A48BB444C78140CC639F1843A499B374D536FC57DDC3C9A31D7255F941923BE4B807", + "ctid": "C00136FE00000066", + "date": 830292660, + "ledger_index": 79614 +} + +=== Reassigning reserve sponsorship... === +Submitting SponsorshipTransfer transaction... +Sponsorship reassigned from Sponsor A (rM8C8CsPpnwno5NZ5XqU6gknvkzveUku21) to Sponsor B (rD8djknRvCyqGi4GTZ1826pxJsNTe8Sw1g)! +{ + "Account": "rhpYcHHetQdCnMfKn4dvnAtR4fpKSTMyjU", + "Fee": "10", + "Flags": 4, + "LastLedgerSequence": 79635, + "ObjectID": "C7E9F77AE8D414E4537715D4C58C9BA5B524D2E3F84A8C47FC70BA7EF6F0B001", + "Sequence": 79612, + "SigningPubKey": "ED5DD35377B24FFF8AEEDDC3BA9B68E89FB1BEDAC91524322D1066A9BC0EFFCCA5", + "Sponsor": "rD8djknRvCyqGi4GTZ1826pxJsNTe8Sw1g", + "SponsorFlags": 2, + "SponsorSignature": { + "SigningPubKey": "ED4F373BA1B49040A55F81FA5C8071FCAC6098DB7EF2364294EC571E90F5516065", + "TxnSignature": "02086F3525A2771885FC047A7D23548FDC5A88F69F5F69C42DBA6214B4467B182E2F892CD860C6324AA6DA6956BC4921B6A84FB610EE80F8A6C5CD479720D208" + }, + "TransactionType": "SponsorshipTransfer", + "TxnSignature": "4C7265A1886DC99B909395BC76E1FD74D1140375458FC6AB48705A01C4C5E3DD8432ACBDB80066C9C10B55E14C690E010FCC5712587CCBA691DC532EC45E2104", + "ctid": "C001370000000066", + "date": 830292662, + "ledger_index": 79616 +} + +=== Ending reserve sponsorship... === +Submitting SponsorshipTransfer transaction... +Reserve sponsorship ended successfully! +{ + "Account": "rhpYcHHetQdCnMfKn4dvnAtR4fpKSTMyjU", + "Fee": "10", + "Flags": 1, + "LastLedgerSequence": 79637, + "ObjectID": "C7E9F77AE8D414E4537715D4C58C9BA5B524D2E3F84A8C47FC70BA7EF6F0B001", + "Sequence": 79613, + "SigningPubKey": "ED5DD35377B24FFF8AEEDDC3BA9B68E89FB1BEDAC91524322D1066A9BC0EFFCCA5", + "TransactionType": "SponsorshipTransfer", + "TxnSignature": "EF8378545F7FB48C71833EEBC97A7CC43D3D4F4598FCBEA88F5D2364E504AB844F57D4661A5A16BB4C82BBD3A2843ADD3D4D4EDFC4EE7A585F930919A07AFD0C", + "ctid": "C001370200000066", + "date": 830292664, + "ledger_index": 79618 +} +``` + +## Manage a Sponsorship Pool + +```sh +python manage_sponsorship_pool.py +``` + +The script should output the pool creation, partial consumption, update, and deletion with balance verification: + +```sh +=== Loading setup data... === + +MPT Issuance ID: 000189E54C940954D5B3E7EC8BB85C4A48F5A6E7A32226CB +Issuer address: rfyu6LEAy6TEqoGabKUT6J22nn8cCgi7qn + +=== Funding accounts... === +Sponsor address: r4bYbadz4mgDhniFk7wkwJ3cP4gXaNw1vu + balance: 100000000 drops + +Sponsee address: rLjWLF1a6CGMK8p5cevXAC4rW7q5Ad1cZT + +=== Creating sponsorship pool... === +Sponsorship pool created successfully: + Fee allocated: 1000000 drops (1 XRP) + Reserves allocated: 5 + +Sponsor balance: 98999990 drops + +=== Using sponsorship pool... === +Pool status after usage: + Fee remaining: 999990 drops + Reserves remaining: 4 + +Sponsorship pool partially consumed! + +=== Updating sponsorship pool... === +Pool status after update: + Fee allocated: 2000000 drops + Reserves allocated: 10 + +Sponsorship pool updated successfully! +{ + "Account": "r4bYbadz4mgDhniFk7wkwJ3cP4gXaNw1vu", + "Fee": "10", + "FeeAmount": "2000000", + "LastLedgerSequence": 100904, + "ReserveCount": 10, + "Sequence": 100878, + "SigningPubKey": "ED5E89EB74FD36F95C4BE249C63C0B1F36C47CD7AA2AC85891E4F8AEB633199229", + "Sponsee": "rLjWLF1a6CGMK8p5cevXAC4rW7q5Ad1cZT", + "TransactionType": "SponsorshipSet", + "TxnSignature": "7FB390CEB0EF276740F8CAB8BF25E28C1C7E4EA335B0FDB69E6A2A297B23F1FF02E34248672F68FA634567AC7E43F257177BD9F9C7561AD9CB3CEBC7C281970C", + "ctid": "C0018A1500000066", + "date": 830354179, + "ledger_index": 100885 +} + +=== Checking sponsor balance before deletion... === +Sponsor balance: 97999970 drops + +=== Deleting sponsorship pool... === +Sponsorship pool deleted successfully! +{ + "Account": "r4bYbadz4mgDhniFk7wkwJ3cP4gXaNw1vu", + "Fee": "10", + "Flags": 1048576, + "LastLedgerSequence": 100906, + "Sequence": 100879, + "SigningPubKey": "ED5E89EB74FD36F95C4BE249C63C0B1F36C47CD7AA2AC85891E4F8AEB633199229", + "Sponsee": "rLjWLF1a6CGMK8p5cevXAC4rW7q5Ad1cZT", + "TransactionType": "SponsorshipSet", + "TxnSignature": "F5C0F7EA883B7CA8C436E94502791A0C8ADD4A70179F0DF73F0DEF4C521E92D544A553155A76BFCC6E7BD949E9F5E7327E7B7A6DCB81D0CCF0A931D3FCB03E0B", + "ctid": "C0018A1700000066", + "date": 830354181, + "ledger_index": 100887 +} + +Sponsor balance: 99999960 drops +Delete transaction fee: 10 drops +Funds returned from pool: 2000000 drops +``` diff --git a/_code-samples/sponsored-fees-and-reserves/py/manage_sponsorship_pool.py b/_code-samples/sponsored-fees-and-reserves/py/manage_sponsorship_pool.py new file mode 100644 index 00000000..6f841620 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/manage_sponsorship_pool.py @@ -0,0 +1,178 @@ +import json +import os +import subprocess +import sys + +from xrpl.clients import JsonRpcClient +from xrpl.models import AccountInfo, MPTokenAuthorize, Payment, SponsorshipSet +from xrpl.models.transactions.sponsorship_set import SponsorshipSetFlag +from xrpl.transaction import submit_and_wait +from xrpl.wallet import Wallet + +SETUP_JSON_FILE = "sponsored_fees_and_reserves.json" +GENESIS_SEED = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb" + +SPF_SPONSOR_FEE = 0x00000001 +SPF_SPONSOR_RESERVE = 0x00000002 + +client = JsonRpcClient("http://localhost:5005") + +# Load setup data -------------------------------------------------------------- +print("=== Loading setup data... ===") +if not os.path.exists(SETUP_JSON_FILE): + print(f"{SETUP_JSON_FILE} not found. Running setup script...") + subprocess.run([sys.executable, "sponsored_fees_and_reserves_setup.py"], check=True) + +with open(SETUP_JSON_FILE) as f: + setup_data = json.load(f) + +mpt_issuance_id = setup_data["mpt_issuance_id"] +issuer_address = setup_data["issuer"]["address"] +print(f"\nMPT Issuance ID: {mpt_issuance_id}") +print(f"Issuer address: {issuer_address}") + +# Fund accounts ---------------------------------------------------------------- +print("\n=== Funding accounts... ===") +genesis = Wallet.from_seed(GENESIS_SEED, algorithm="secp256k1") +sponsor = Wallet.create() +sponsee = Wallet.create() + +for wallet, name in [(sponsor, "sponsor"), (sponsee, "sponsee")]: + fund_tx = Payment( + account=genesis.address, + destination=wallet.address, + amount="100000000", # 100 XRP + ) + response = submit_and_wait(fund_tx, client, genesis, autofill=True) + if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + print(f"Error funding {name}: {response.result['meta']['TransactionResult']}") + sys.exit(1) + +sponsor_info = client.request(AccountInfo(account=sponsor.address)) +print(f"Sponsor address: {sponsor.address}") +print(f" balance: {sponsor_info.result['account_data']['Balance']} drops") +print(f"\nSponsee address: {sponsee.address}") + + +# Create a sponsorship pool ---------------------------------------------------- +# The sponsor creates a pre-funded pool for the sponsee with fees and reserves. +print("\n=== Creating sponsorship pool... ===") +pool_tx = SponsorshipSet( + account=sponsor.address, + sponsee=sponsee.address, + fee_amount="1000000", # 1 XRP in drops + reserve_count=5, +) +mpt_auth_response = submit_and_wait(pool_tx, client, sponsor, autofill=True) +if mpt_auth_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = mpt_auth_response.result["meta"]["TransactionResult"] + print(f"Error creating sponsorship pool: {result_code}") + sys.exit(1) + +sponsor_info = client.request(AccountInfo(account=sponsor.address)) +sponsor_balance = int(sponsor_info.result["account_data"]["Balance"]) + +print("Sponsorship pool created successfully:") +print(f" Fee allocated: 1000000 drops (1 XRP)") +print(f" Reserves allocated: 5") +print(f"\nSponsor balance: {sponsor_balance} drops") + +# Use the sponsorship pool ----------------------------------------------------- +# The sponsee submits an MPTokenAuthorize transaction using the pool, partially consuming it. +print("\n=== Using sponsorship pool... ===") +authorize_tx = MPTokenAuthorize( + account=sponsee.address, + mptoken_issuance_id=mpt_issuance_id, + sponsor=sponsor.address, + sponsor_flags=SPF_SPONSOR_FEE | SPF_SPONSOR_RESERVE, +) +response = submit_and_wait(authorize_tx, client, sponsee, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error using sponsorship pool: {result_code}") + sys.exit(1) + +sponsorship_node = next( + (node for node in response.result["meta"]["AffectedNodes"] + if node.get("ModifiedNode", {}).get("LedgerEntryType") == "Sponsorship"), + None, +) +if sponsorship_node: + fields = sponsorship_node["ModifiedNode"]["FinalFields"] + print(f"Pool status after usage:") + print(f" Fee remaining: {fields.get('FeeAmount', '0')} drops") + print(f" Reserves remaining: {fields.get('ReserveCount', 0)}") + +print("\nSponsorship pool partially consumed!") + +# Update the sponsorship pool -------------------------------------------------- +# The sponsor tops up the fee allocation and adds more reserve slots. +print("\n=== Updating sponsorship pool... ===") +update_tx = SponsorshipSet( + account=sponsor.address, + sponsee=sponsee.address, + fee_amount="2000000", # increase to 2 XRP in drops + reserve_count=10, +) +update_response = submit_and_wait(update_tx, client, sponsor, autofill=True) +if update_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = update_response.result["meta"]["TransactionResult"] + print(f"Error updating sponsorship pool: {result_code}") + sys.exit(1) + +sponsorship_node = next( + (node for node in update_response.result["meta"]["AffectedNodes"] + if node.get("ModifiedNode", {}).get("LedgerEntryType") == "Sponsorship"), + None, +) +if sponsorship_node: + fields = sponsorship_node["ModifiedNode"]["FinalFields"] + print(f"Pool status after update:") + print(f" Fee allocated: {fields.get('FeeAmount', '0')} drops") + print(f" Reserves allocated: {fields.get('ReserveCount', 0)}") + +print("\nSponsorship pool updated successfully!") +print(json.dumps(update_response.result["tx_json"], indent=2)) + +# Check sponsor balance before deletion ---------------------------------------- +print("\n=== Checking sponsor balance before deletion... ===") +sponsor_info = client.request(AccountInfo(account=sponsor.address)) +balance_before = int(sponsor_info.result["account_data"]["Balance"]) +print(f"Sponsor balance: {balance_before} drops") + +# Delete the sponsorship pool -------------------------------------------------- +# The sponsor deletes the pool to reclaim any remaining funds. +print("\n=== Deleting sponsorship pool... ===") +delete_tx = SponsorshipSet( + account=sponsor.address, + sponsee=sponsee.address, + flags=SponsorshipSetFlag.TF_DELETE_OBJECT, +) +submit_response = submit_and_wait(delete_tx, client, sponsor, autofill=True) +if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = submit_response.result["meta"]["TransactionResult"] + print(f"Error deleting sponsorship pool: {result_code}") + sys.exit(1) + +# Verify the Sponsorship object was deleted ------------------------------------ +deleted_node = next( + (node for node in submit_response.result["meta"]["AffectedNodes"] + if node.get("DeletedNode", {}).get("LedgerEntryType") == "Sponsorship"), + None, +) +if deleted_node is None: + print("Error: Sponsorship object not deleted") + sys.exit(1) + +print("Sponsorship pool deleted successfully!") +print(json.dumps(submit_response.result["tx_json"], indent=2)) + +# Check sponsor balance after deletion ----------------------------------------- +sponsor_info = client.request(AccountInfo(account=sponsor.address)) +balance_after = int(sponsor_info.result["account_data"]["Balance"]) +delete_fee = int(submit_response.result["tx_json"]["Fee"]) +funds_returned = balance_after - balance_before + delete_fee + +print(f"\nSponsor balance: {balance_after} drops") +print(f"Delete transaction fee: {delete_fee} drops") +print(f"Funds returned from pool: {funds_returned} drops") diff --git a/_code-samples/sponsored-fees-and-reserves/py/requirements.txt b/_code-samples/sponsored-fees-and-reserves/py/requirements.txt new file mode 100644 index 00000000..386c7952 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/requirements.txt @@ -0,0 +1,4 @@ +# Install xrpl-py from the sponsorship PR +# This is required for XLS-68 (Sponsored Fees and Reserves) support +xrpl-py @ git+https://github.com/XRPLF/xrpl-py.git@refs/pull/921/head + diff --git a/_code-samples/sponsored-fees-and-reserves/py/sponsor_co_signed.py b/_code-samples/sponsored-fees-and-reserves/py/sponsor_co_signed.py new file mode 100644 index 00000000..b3803e73 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/sponsor_co_signed.py @@ -0,0 +1,172 @@ +import json +import os +import subprocess +import sys + +from xrpl.clients import JsonRpcClient +from xrpl.models import Payment, MPTokenAuthorize, PaymentFlag +from xrpl.transaction import autofill, sign, sign_as_sponsor, submit_and_wait +from xrpl.wallet import Wallet + +SETUP_JSON_FILE = "sponsored_fees_and_reserves.json" +GENESIS_SEED = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb" + +SPF_SPONSOR_FEE = 0x00000001 +SPF_SPONSOR_RESERVE = 0x00000002 + +client = JsonRpcClient("http://localhost:5005") + +# Load setup data +print("=== Loading setup data... ===") +if not os.path.exists(SETUP_JSON_FILE): + print(f"{SETUP_JSON_FILE} not found. Running setup script...") + subprocess.run([sys.executable, "sponsored_fees_and_reserves_setup.py"], check=True) + +with open(SETUP_JSON_FILE) as f: + setup_data = json.load(f) + +mpt_issuance_id = setup_data["mpt_issuance_id"] +issuer_address = setup_data["issuer"]["address"] +print(f"\nMPT Issuance ID: {mpt_issuance_id}") +print(f"Issuer address: {issuer_address}") + +print("\n=== Creating wallets... ===") +genesis = Wallet.from_seed(GENESIS_SEED, algorithm="secp256k1") +sponsor = Wallet.create() +sponsee = Wallet.create() + +# Fund the sponsor only +fund_tx = Payment( + account=genesis.address, + destination=sponsor.address, + amount="100000000", # 100 XRP +) +response = submit_and_wait(fund_tx, client, genesis, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + print(f"Error funding sponsor: {response.result['meta']['TransactionResult']}") + sys.exit(1) + +print(f"Sponsor address: {sponsor.address}") +print("Funded sponsor with 100 XRP") +print(f"\nSponsee address: {sponsee.address}") + +# Sponsor creates the sponsee's account ---------------------------------------- +print("\n=== Creating sponsee's account... ===") +fund_account_tx = Payment( + account=sponsor.address, + destination=sponsee.address, + amount="1", # Minimal amount required + flags=PaymentFlag.TF_SPONSOR_CREATED_ACCOUNT, # Sponsor pays the reserve +) +print(json.dumps(fund_account_tx.to_xrpl(), indent=2)) + +response = submit_and_wait(fund_account_tx, client, sponsor, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error funding sponsee address: {result_code}") + sys.exit(1) + +# Verify the sponsee's account was created +account_node = next( + (node for node in response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "AccountRoot"), + None, +) +if account_node is None: + print("Error: AccountRoot not found in metadata") + sys.exit(1) + +new_account = account_node["CreatedNode"]["NewFields"] +if new_account.get("Account") != sponsee.address: + print("Error: AccountRoot address does not match sponsee") + sys.exit(1) +if new_account.get("Sponsor") != sponsor.address: + print("Error: AccountRoot Sponsor does not match sponsor") + sys.exit(1) + +print("\nSponsee account created successfully!") + +# Prepare the sponsored transaction ------------------------------------ +print("\n=== Preparing sponsored transaction... ===") +mptoken_authorize_tx = MPTokenAuthorize( + account=sponsee.address, + mptoken_issuance_id=mpt_issuance_id, + sponsor=sponsor.address, + sponsor_flags=SPF_SPONSOR_FEE | SPF_SPONSOR_RESERVE, +) +mptoken_authorize_tx = autofill(mptoken_authorize_tx, client) + +# The sponsee signs, then the sponsor co-signs to cover fee and reserve. +sponsee_signed_tx = sign(mptoken_authorize_tx, sponsee) +co_signed_tx = sign_as_sponsor(sponsor, sponsee_signed_tx) + +print(json.dumps(co_signed_tx.tx.to_xrpl(), indent=2)) + +# Submit the sponsored transaction --------------------------------------- +print("\n=== Submitting sponsored transaction... ===") +submit_response = submit_and_wait(co_signed_tx.tx, client) +if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = submit_response.result["meta"]["TransactionResult"] + print(f"Error: Holder MPT authorization failed: {result_code}") + sys.exit(1) + +# Verify the transaction was sponsored by checking the relevant fields +tx_json = submit_response.result["tx_json"] +if tx_json.get("Sponsor") != sponsor.address: + print("Error: Sponsor field mismatch") + sys.exit(1) +if tx_json.get("SponsorFlags") != SPF_SPONSOR_FEE | SPF_SPONSOR_RESERVE: + print("Error: SponsorFlags mismatch") + sys.exit(1) +if "SponsorSignature" not in tx_json: + print("Error: SponsorSignature missing") + sys.exit(1) + +# Check the MPToken was created with the sponsor field +mptoken_node = next( + (node for node in submit_response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "MPToken"), + None, +) +if mptoken_node is None: + print("Error: MPToken not found in metadata") + sys.exit(1) +if mptoken_node["CreatedNode"]["NewFields"].get("Sponsor") != sponsor.address: + print("Error: MPToken Sponsor field mismatch") + sys.exit(1) + +print("Transaction successfully sponsored!") +print(json.dumps(submit_response.result["tx_json"], indent=2)) + +# Show who paid what by inspecting the affected AccountRoot nodes +details = {"sponsor": {}, "sponsee": {}} + +for node in submit_response.result["meta"]["AffectedNodes"]: + if "ModifiedNode" not in node: + continue + modified = node["ModifiedNode"] + fields = modified["FinalFields"] + prev = modified.get("PreviousFields", {}) + + if modified["LedgerEntryType"] == "AccountRoot": + if fields["Account"] == sponsor.address: + details["sponsor"] = { + "fee": int(prev["Balance"]) - int(fields["Balance"]) if "Balance" in prev else 0, + "balance": fields["Balance"], + "reserves": fields.get("SponsoringOwnerCount", 0), + } + if fields["Account"] == sponsee.address: + details["sponsee"] = { + "fee": int(prev["Balance"]) - int(fields["Balance"]) if "Balance" in prev else 0, + "balance": fields["Balance"], + } + +print("\nSponsorship details --------------------------------------") +print(f" Sponsor:") +print(f" Fee deducted: {details['sponsor'].get('fee', 0)} drops") +print(f" Balance: {details['sponsor'].get('balance', 0)} drops") +print(f" Reserves sponsored: {details['sponsor'].get('reserves', 0)}") +print(f"\n Sponsee:") +print(f" Fee deducted: {details['sponsee'].get('fee', 0)} drops") +print(f" Balance: {details['sponsee'].get('balance', 0)} drops") + diff --git a/_code-samples/sponsored-fees-and-reserves/py/sponsor_pre_funded.py b/_code-samples/sponsored-fees-and-reserves/py/sponsor_pre_funded.py new file mode 100644 index 00000000..e4a71eb9 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/sponsor_pre_funded.py @@ -0,0 +1,205 @@ +import json +import os +import subprocess +import sys + +from xrpl.clients import JsonRpcClient +from xrpl.models import Payment, MPTokenAuthorize, PaymentFlag, SponsorshipSet +from xrpl.transaction import submit_and_wait +from xrpl.wallet import Wallet + +SETUP_JSON_FILE = "sponsored_fees_and_reserves.json" +GENESIS_SEED = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb" + +SPF_SPONSOR_FEE = 0x00000001 +SPF_SPONSOR_RESERVE = 0x00000002 + +client = JsonRpcClient("http://localhost:5005") + +# Load setup data +print("=== Loading setup data... ===") +if not os.path.exists(SETUP_JSON_FILE): + print(f"{SETUP_JSON_FILE} not found. Running setup script...") + subprocess.run([sys.executable, "sponsored_fees_and_reserves_setup.py"], check=True) + +with open(SETUP_JSON_FILE) as f: + setup_data = json.load(f) + +mpt_issuance_id = setup_data["mpt_issuance_id"] +issuer_address = setup_data["issuer"]["address"] +print(f"\nMPT Issuance ID: {mpt_issuance_id}") +print(f"Issuer address: {issuer_address}") + +print("\n=== Creating wallets... ===") +genesis = Wallet.from_seed(GENESIS_SEED, algorithm="secp256k1") +sponsor = Wallet.create() +sponsee = Wallet.create() + +# Fund the sponsor only +fund_tx = Payment( + account=genesis.address, + destination=sponsor.address, + amount="100000000", # 100 XRP +) +response = submit_and_wait(fund_tx, client, genesis, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + print(f"Error funding sponsor: {response.result['meta']['TransactionResult']}") + sys.exit(1) + +print(f"Sponsor address: {sponsor.address}") +print("Funded sponsor with 100 XRP") +print(f"\nSponsee address: {sponsee.address}") + +# Sponsor creates the sponsee's account ---------------------------------------- +print("\n=== Creating sponsee's account... ===") +fund_account_tx = Payment( + account=sponsor.address, + destination=sponsee.address, + amount="1", # Minimal amount - reserve is sponsored, not transferred + flags=PaymentFlag.TF_SPONSOR_CREATED_ACCOUNT, +) +print(json.dumps(fund_account_tx.to_xrpl(), indent=2)) + +response = submit_and_wait(fund_account_tx, client, sponsor, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error funding sponsee address: {result_code}") + sys.exit(1) + +# Verify the sponsee's account was created +account_node = next( + (node for node in response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "AccountRoot"), + None, +) +if account_node is None: + print("Error: AccountRoot not found in metadata") + sys.exit(1) + +new_account = account_node["CreatedNode"]["NewFields"] +if new_account.get("Account") != sponsee.address: + print("Error: AccountRoot address does not match sponsee") + sys.exit(1) +if new_account.get("Sponsor") != sponsor.address: + print("Error: AccountRoot Sponsor does not match sponsor") + sys.exit(1) + +print("\nSponsee account created successfully!") + +# Create the Sponsorship object ------------------------------------------------ +print("\n=== Creating sponsorship pool... ===") +sponsorship_set_tx = SponsorshipSet( + account=sponsor.address, + sponsee=sponsee.address, + fee_amount="1000000", # 1 XRP in drops for transaction fees + reserve_count=5, # 5 owner count increments for reserves +) +print(json.dumps(sponsorship_set_tx.to_xrpl(), indent=2)) + +# Submit the SponsorshipSet transaction ---------------------------------------- +sponsorship_response = submit_and_wait(sponsorship_set_tx, client, sponsor, autofill=True) +if sponsorship_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = sponsorship_response.result["meta"]["TransactionResult"] + print(f"Error creating sponsorship pool: {result_code}") + sys.exit(1) + +# Verify the Sponsorship object was created +sponsorship_node = next( + (node for node in sponsorship_response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "Sponsorship"), + None, +) +if sponsorship_node is None: + print("Error: Sponsorship object not found in metadata") + sys.exit(1) + +sponsorship_id = sponsorship_node["CreatedNode"]["LedgerIndex"] +print(f"\nSponsorship pool created successfully!") +print(f"Sponsorship ID: {sponsorship_id}") + +# Use the pool to submit a sponsored transaction -------------------------- +# The sponsee references the sponsor, but does NOT need the sponsor's signature. +print("\n=== Submitting sponsored transaction... ===") +mptoken_authorize_tx = MPTokenAuthorize( + account=sponsee.address, + mptoken_issuance_id=mpt_issuance_id, + sponsor=sponsor.address, + sponsor_flags=SPF_SPONSOR_FEE | SPF_SPONSOR_RESERVE, +) +submit_response = submit_and_wait(mptoken_authorize_tx, client, sponsee, autofill=True) +if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = submit_response.result["meta"]["TransactionResult"] + print(f"Error: MPTokenAuthorize failed: {result_code}") + sys.exit(1) + +# Verify the transaction was sponsored +tx_json = submit_response.result["tx_json"] +if tx_json.get("Sponsor") != sponsor.address: + print("Error: Sponsor field mismatch") + sys.exit(1) +if tx_json.get("SponsorFlags") != SPF_SPONSOR_FEE | SPF_SPONSOR_RESERVE: + print("Error: SponsorFlags mismatch") + sys.exit(1) +if "SponsorSignature" in tx_json: + print("Error: SponsorSignature should not be present in pre-funded flow") + sys.exit(1) + +# Verify the MPToken was created with the sponsor field +mptoken_node = next( + (node for node in submit_response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "MPToken"), + None, +) +if mptoken_node is None: + print("Error: MPToken not found in metadata") + sys.exit(1) +if mptoken_node["CreatedNode"]["NewFields"].get("Sponsor") != sponsor.address: + print("Error: MPToken Sponsor field mismatch") + sys.exit(1) + +print("Transaction successfully sponsored!") +print(json.dumps(submit_response.result["tx_json"], indent=2)) + +# Show who paid what by inspecting the affected nodes +details = {"sponsor": {}, "sponsee": {}, "pool": {}} + +for node in submit_response.result["meta"]["AffectedNodes"]: + if "ModifiedNode" not in node: + continue + modified = node["ModifiedNode"] + fields = modified["FinalFields"] + prev = modified.get("PreviousFields", {}) + + if modified["LedgerEntryType"] == "AccountRoot": + if fields["Account"] == sponsor.address: + details["sponsor"] = { + "fee": int(prev["Balance"]) - int(fields["Balance"]) if "Balance" in prev else 0, + "balance": fields["Balance"], + "reserves": fields.get("SponsoringOwnerCount", 0), + } + if fields["Account"] == sponsee.address: + details["sponsee"] = { + "fee": int(prev["Balance"]) - int(fields["Balance"]) if "Balance" in prev else 0, + "balance": fields["Balance"], + } + + if modified["LedgerEntryType"] == "Sponsorship": + details["pool"] = { + "fee": int(prev["FeeAmount"]) - int(fields["FeeAmount"]) if "FeeAmount" in prev else 0, + "balance": fields["FeeAmount"], + "reserves_consumed": prev["ReserveCount"] - fields["ReserveCount"] if "ReserveCount" in prev else 0, + } + +print("\nSponsorship details --------------------------------------") +print(f" Pool:") +print(f" Fee deducted: {details['pool'].get('fee', 0)} drops") +print(f" Balance: {details['pool'].get('balance', 0)} drops") +print(f" Reserves consumed: {details['pool'].get('reserves_consumed', 0)}") +print(f"\n Sponsor:") +print(f" Fee deducted: {details['sponsor'].get('fee', 0)} drops") +print(f" Balance: {details['sponsor'].get('balance', 0)} drops") +print(f" Reserves sponsored: {details['sponsor'].get('reserves', 0)}") +print(f"\n Sponsee:") +print(f" Fee deducted: {details['sponsee'].get('fee', 0)} drops") +print(f" Balance: {details['sponsee'].get('balance', 0)} drops") + diff --git a/_code-samples/sponsored-fees-and-reserves/py/sponsored_fees_and_reserves_setup.py b/_code-samples/sponsored-fees-and-reserves/py/sponsored_fees_and_reserves_setup.py new file mode 100644 index 00000000..5a9b100f --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/sponsored_fees_and_reserves_setup.py @@ -0,0 +1,72 @@ +import json +import sys + +from xrpl.clients import JsonRpcClient +from xrpl.models import ( + MPTokenIssuanceCreate, + MPTokenIssuanceCreateFlag, + Payment, +) +from xrpl.transaction import submit_and_wait +from xrpl.wallet import Wallet + +ACCOUNTS_FILE = "sponsored_fees_and_reserves.json" +GENESIS_SEED = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb" + +client = JsonRpcClient("http://localhost:5005") +genesis = Wallet.from_seed(GENESIS_SEED, algorithm="secp256k1") + +def fund_account(wallet, name): + """Fund an account from genesis.""" + fund_tx = Payment( + account=genesis.address, + destination=wallet.address, + amount="100000000", # 100 XRP + ) + response = submit_and_wait(fund_tx, client, genesis, autofill=True) + if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + print(f"Error funding {name}: {response.result['meta']['TransactionResult']}") + sys.exit(1) + + +print("Setting up tutorials: 0/3", end="\r") + +# 1. Create and fund issuer --------------------------------------------------- +issuer = Wallet.create() +fund_account(issuer, "issuer") + +print("Setting up tutorials: 1/3", end="\r") + +# 2. Create MPT issuance ------------------------------------------------------ +mpt_create_tx = MPTokenIssuanceCreate( + account=issuer.address, + maximum_amount="1000000000000", + asset_scale=2, + flags=( + MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRANSFER + | MPTokenIssuanceCreateFlag.TF_MPT_CAN_LOCK + | MPTokenIssuanceCreateFlag.TF_MPT_REQUIRE_AUTH + ), + transfer_fee=0, +) +response = submit_and_wait(mpt_create_tx, client, issuer, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + print(f"Error creating MPT: {response.result['meta']['TransactionResult']}") + sys.exit(1) + +mpt_issuance_id = response.result["meta"]["mpt_issuance_id"] + +print("Setting up tutorials: 2/3", end="\r") + +# 3. Save setup data to JSON -------------------------------------------------- +setup_data = { + "description": "Auto-generated by sponsored_fees_and_reserves_setup.py. " + "Stores XRPL account info for sponsored fees tutorials.", + "issuer": {"address": issuer.address, "seed": issuer.seed}, + "mpt_issuance_id": mpt_issuance_id, +} + +with open(ACCOUNTS_FILE, "w") as f: + json.dump(setup_data, f, indent=2) + +print("Setting up tutorials: Complete!") diff --git a/_code-samples/sponsored-fees-and-reserves/py/transfer_sponsorship.py b/_code-samples/sponsored-fees-and-reserves/py/transfer_sponsorship.py new file mode 100644 index 00000000..e3358bd7 --- /dev/null +++ b/_code-samples/sponsored-fees-and-reserves/py/transfer_sponsorship.py @@ -0,0 +1,187 @@ +import json +import os +import subprocess +import sys + +from xrpl.clients import JsonRpcClient +from xrpl.models import MPTokenAuthorize, Payment, SponsorshipTransfer +from xrpl.models.transactions.sponsorship_transfer import SponsorshipTransferFlag +from xrpl.transaction import autofill, sign, sign_as_sponsor, submit_and_wait +from xrpl.wallet import Wallet + +SETUP_JSON_FILE = "sponsored_fees_and_reserves.json" +GENESIS_SEED = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb" + +SPF_SPONSOR_RESERVE = 0x00000002 + +client = JsonRpcClient("http://localhost:5005") + +# Load setup data -------------------------------------------------------------- +print("=== Loading setup data... ===") +if not os.path.exists(SETUP_JSON_FILE): + print(f"{SETUP_JSON_FILE} not found. Running setup script...") + subprocess.run([sys.executable, "sponsored_fees_and_reserves_setup.py"], check=True) + +with open(SETUP_JSON_FILE) as f: + setup_data = json.load(f) + +mpt_issuance_id = setup_data["mpt_issuance_id"] +issuer_address = setup_data["issuer"]["address"] +print(f"MPT Issuance ID: {mpt_issuance_id}") +print(f"Issuer address: {issuer_address}") + +# Fund accounts ---------------------------------------------------------------- +print("\n=== Funding accounts... ===") +genesis = Wallet.from_seed(GENESIS_SEED, algorithm="secp256k1") +sponsor_a = Wallet.create() +sponsor_b = Wallet.create() +sponsee = Wallet.create() + +for wallet, name in [(sponsor_a, "sponsor_a"), (sponsor_b, "sponsor_b"), (sponsee, "sponsee")]: + fund_tx = Payment( + account=genesis.address, + destination=wallet.address, + amount="100000000", # 100 XRP + ) + response = submit_and_wait(fund_tx, client, genesis, autofill=True) + if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + print(f"Error funding {name}: {response.result['meta']['TransactionResult']}") + sys.exit(1) + +print(f"Sponsor A address: {sponsor_a.address}") +print(f"Sponsor B address: {sponsor_b.address}") +print(f"Sponsee address: {sponsee.address}") + +# Submit an unsponsored MPTokenAuthorize transaction ------------------------------------------------ +# The sponsee authorizes an MPToken without any sponsorship. +print("\n=== Submitting unsponsored MPTokenAuthorize transaction... ===") +mptoken_authorize_tx = MPTokenAuthorize( + account=sponsee.address, + mptoken_issuance_id=mpt_issuance_id, +) +response = submit_and_wait(mptoken_authorize_tx, client, sponsee, autofill=True) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error creating MPToken: {result_code}") + sys.exit(1) + +mptoken_node = next( + (node for node in response.result["meta"]["AffectedNodes"] + if node.get("CreatedNode", {}).get("LedgerEntryType") == "MPToken"), + None, +) +if mptoken_node is None: + print("Error: MPToken not found in metadata") + sys.exit(1) + +print(f"MPTokenAuthorize transaction successful!") +mptoken_id = mptoken_node["CreatedNode"]["LedgerIndex"] +print(f"MPToken ID: {mptoken_id}") + +# Create a reserve sponsorship ------------------------------------------------- +# Sponsor A creates a sponsorship on the existing unsponsored MPToken. +print("\n=== Creating reserve sponsorship... ===") +create_tx = SponsorshipTransfer( + account=sponsee.address, + object_id=mptoken_id, + flags=SponsorshipTransferFlag.TF_SPONSORSHIP_CREATE, + sponsor=sponsor_a.address, + sponsor_flags=SPF_SPONSOR_RESERVE, +) +create_tx = autofill(create_tx, client) +sponsee_signed = sign(create_tx, sponsee) +co_signed = sign_as_sponsor(sponsor_a, sponsee_signed) + +print("Submitting SponsorshipTransfer transaction...") +response = submit_and_wait(co_signed.tx, client) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error creating sponsorship: {result_code}") + sys.exit(1) + +# Verify the sponsorship was created +mptoken_modified = next( + (node for node in response.result["meta"]["AffectedNodes"] + if node.get("ModifiedNode", {}).get("LedgerEntryType") == "MPToken"), + None, +) +if mptoken_modified is None: + print("Error: MPToken not found in metadata") + sys.exit(1) +new_sponsor = mptoken_modified["ModifiedNode"]["FinalFields"].get("Sponsor") +if new_sponsor != sponsor_a.address: + print(f"Error: Expected Sponsor {sponsor_a.address}, got {new_sponsor}") + sys.exit(1) + +print(f"Sponsor A ({sponsor_a.address}) is now sponsoring the MPToken reserve!") +print(json.dumps(response.result["tx_json"], indent=2)) + +# Reassign the reserve sponsorship --------------------------------------------- +# Transfer the reserve sponsorship from Sponsor A to Sponsor B. +print("\n=== Reassigning reserve sponsorship... ===") +reassign_tx = SponsorshipTransfer( + account=sponsee.address, + object_id=mptoken_id, + flags=SponsorshipTransferFlag.TF_SPONSORSHIP_REASSIGN, + sponsor=sponsor_b.address, + sponsor_flags=SPF_SPONSOR_RESERVE, +) +reassign_tx = autofill(reassign_tx, client) +sponsee_signed = sign(reassign_tx, sponsee) +co_signed = sign_as_sponsor(sponsor_b, sponsee_signed) + +print("Submitting SponsorshipTransfer transaction...") +response = submit_and_wait(co_signed.tx, client) +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error reassigning sponsorship: {result_code}") + sys.exit(1) + +# Verify the sponsorship was reassigned +mptoken_modified = next( + (node for node in response.result["meta"]["AffectedNodes"] + if node.get("ModifiedNode", {}).get("LedgerEntryType") == "MPToken"), + None, +) +if mptoken_modified is None: + print("Error: MPToken not found in metadata") + sys.exit(1) +new_sponsor = mptoken_modified["ModifiedNode"]["FinalFields"].get("Sponsor") +if new_sponsor != sponsor_b.address: + print(f"Error: Expected Sponsor {sponsor_b.address}, got {new_sponsor}") + sys.exit(1) + +print(f"Sponsorship reassigned from Sponsor A ({sponsor_a.address}) to Sponsor B ({sponsor_b.address})!") +print(json.dumps(response.result["tx_json"], indent=2)) + +# End the reserve sponsorship -------------------------------------------------- +# The sponsee takes over the reserve obligation for the MPToken. +print("\n=== Ending reserve sponsorship... ===") +end_tx = SponsorshipTransfer( + account=sponsee.address, + object_id=mptoken_id, + flags=SponsorshipTransferFlag.TF_SPONSORSHIP_END, +) + +print("Submitting SponsorshipTransfer transaction...") +submit_response = submit_and_wait(end_tx, client, sponsee, autofill=True) +if submit_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = submit_response.result["meta"]["TransactionResult"] + print(f"Error ending reserve sponsorship: {result_code}") + sys.exit(1) + +# Verify the sponsorship was ended --------------------------------------------- +mptoken_modified = next( + (node for node in submit_response.result["meta"]["AffectedNodes"] + if node.get("ModifiedNode", {}).get("LedgerEntryType") == "MPToken"), + None, +) +if mptoken_modified is None: + print("Error: MPToken not found in metadata") + sys.exit(1) +if "Sponsor" in mptoken_modified["ModifiedNode"]["FinalFields"]: + print("Error: Sponsor field still present on MPToken") + sys.exit(1) + +print("Reserve sponsorship ended successfully!") +print(json.dumps(submit_response.result["tx_json"], indent=2)) diff --git a/docs/_snippets/_sponsor-disclaimer.md b/docs/_snippets/_sponsor-disclaimer.md new file mode 100644 index 00000000..eb547cb8 --- /dev/null +++ b/docs/_snippets/_sponsor-disclaimer.md @@ -0,0 +1,3 @@ +{% admonition type="info" name="Note" %} +Sponsored Fees and Reserves is not enabled on Devnet. To test this feature you can run `rippled` in [stand-alone mode](https://xrpl.org/docs/concepts/networks-and-servers/rippled-server-modes#stand-alone-mode) and enable `Sponsor` in the [rippled.cfg file](https://xrpl.org/docs/infrastructure/testing-and-auditing/test-amendments). +{% /admonition %} diff --git a/docs/_snippets/common-links.md b/docs/_snippets/common-links.md index 65c873e6..be93e0ae 100644 --- a/docs/_snippets/common-links.md +++ b/docs/_snippets/common-links.md @@ -31,7 +31,7 @@ [Specifying Ledgers]: https://xrpl.org/docs/references/protocol/data-types/basic-data-types/#specifying-ledgers [Sponsor amendment]: https://xls.xrpl.org/xls/XLS-0068-sponsored-fees-and-reserves.html [Sponsorship ledger entry]: /docs/xls-68-sponsored-fees-and-reserves/references/ledger-entries/sponsorship.md -[SponsorshipSet]: /docs/xls-68-sponsored-fees-and-reserves/transactions/sponsorshipset.md +[SponsorshipSet]: /docs/xls-68-sponsored-fees-and-reserves/references/transactions/sponsorshipset.md [SponsorshipSet transaction]: /docs/xls-68-sponsored-fees-and-reserves/references/transactions/sponsorshipset.md [SponsorshipTransfer]: /docs/xls-68-sponsored-fees-and-reserves/references/transactions/sponsorshiptransfer.md [SponsorshipTransfer transaction]: /docs/xls-68-sponsored-fees-and-reserves/references/transactions/sponsorshiptransfer.md diff --git a/docs/xls-68-sponsored-fees-and-reserves/index.page.tsx b/docs/xls-68-sponsored-fees-and-reserves/index.page.tsx index 42b3bdae..6e93477d 100644 --- a/docs/xls-68-sponsored-fees-and-reserves/index.page.tsx +++ b/docs/xls-68-sponsored-fees-and-reserves/index.page.tsx @@ -52,7 +52,7 @@ export default function Page() { onKeyDatesUpdate={handleKeyDatesUpdate} /> - + - ); } diff --git a/sidebars.yaml b/sidebars.yaml index bcdf00a3..fba37342 100644 --- a/sidebars.yaml +++ b/sidebars.yaml @@ -74,5 +74,11 @@ - page: docs/xls-68-sponsored-fees-and-reserves/references/apis/account_sponsoring.md - page: docs/xls-68-sponsored-fees-and-reserves/references/apis/updated-apis.md - page: docs/xls-68-sponsored-fees-and-reserves/references/updated-permission-values.md - + - group: Tutorials + expanded: false + items: + - page: docs/xls-68-sponsored-fees-and-reserves/tutorials/sponsor-a-transaction.md + - page: docs/xls-68-sponsored-fees-and-reserves/tutorials/sponsor-a-transaction-with-a-pre-funded-pool.md + - page: docs/xls-68-sponsored-fees-and-reserves/tutorials/transfer-a-reserve-sponsorship.md + - page: docs/xls-68-sponsored-fees-and-reserves/tutorials/manage-a-sponsorship-pool.md - page: docs/xls-82-mpt-dex/index.page.tsx