This tutorial shows you how to use the SponsorshipTransfer transaction to create, reassign, or end reserve sponsorship for an existing ledger entry. This example walks through all three operations on a DepositPreauth entry, but SponsorshipTransfer can also transfer sponsorship of account reserves.
Requires the Sponsor amendment. Loading...
By the end of this tutorial, you should be able to:
- Create a reserve sponsorship on an existing unsponsored ledger entry.
- Reassign a reserve sponsorship from one sponsor to another.
- End a reserve sponsorship so the sponsee covers the reserve.
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger and Sponsored Fees and Reserves.
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
- JavaScript with the xrpl.js library. See Get Started Using JavaScript for setup steps.
- Python with the xrpl-py library. See Get Started Using Python for setup steps.
You can find the complete source code for this tutorial's example in the code samples section of this website's repository.
From the code sample folder, use npm to install dependencies:
npm installImport the necessary libraries and instantiate a client to connect to the XRPL. This example imports:
xrpl: Used for XRPL client connection, transaction submission, and wallet handling.
import {
Client,
SponsorFlags,
SponsorshipTransferFlags,
signAsSponsor,
validate
} from 'xrpl'
// Connect to the network ----------------------
const client = new Client('wss://s.devnet.rippletest.net:51233')
await client.connect()Fund three accounts: Sponsor A, Sponsor B, and the sponsee.
// Create the wallets ----------------------
console.log(`\n=== Creating the sponsor and sponsee wallets... ===`)
const { wallet: sponsorA } = await client.fundWallet()
const { wallet: sponsorB } = await client.fundWallet()
const { wallet: sponsee } = await client.fundWallet()
console.log(`Sponsor A address: ${sponsorA.address}`)
console.log(`Sponsor B address: ${sponsorB.address}`)
console.log(`Sponsee address: ${sponsee.address}`)Submit an unsponsored transaction. For this example, a DepositPreauth transaction.
// Create an unsponsored ledger entry ----------------------
// The sponsee creates a DepositPreauth entry with no sponsorship fields, so it pays
// the fee and the owner reserve for the resulting entry itself.
console.log(`\n=== Submitting unsponsored DepositPreauth transaction... ===`)
const depositPreauthTx = {
TransactionType: 'DepositPreauth',
Account: sponsee.address,
Authorize: sponsorA.address
}
const depositPreauthResponse = await client.submitAndWait(depositPreauthTx, {
wallet: sponsee,
autofill: true
})
if (depositPreauthResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = depositPreauthResponse.result.meta.TransactionResult
console.error('Error: Unable to create the preauthorization:', resultCode)
await client.disconnect()
process.exit(1)
}
const preauthNode = depositPreauthResponse.result.meta.AffectedNodes.find(
node => node.CreatedNode?.LedgerEntryType === 'DepositPreauth'
)
const preauthID = preauthNode.CreatedNode.LedgerIndex
console.log('DepositPreauth created successfully, with its reserve paid by the sponsee.')
console.log(`DepositPreauth ID: ${preauthID}`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${depositPreauthResponse.result.hash}`)Submit a SponsorshipTransfer transaction with the tfSponsorshipCreate flag enabled, the unsponsored ledger entry's ID, and Sponsor A's address in the Sponsor field.
// Prepare SponsorshipTransfer transaction to start the sponsorship ----------------------
console.log(`\n=== Preparing SponsorshipTransfer transaction to start the sponsorship... ===`)
const createTx = {
TransactionType: 'SponsorshipTransfer',
Account: sponsee.address,
ObjectID: preauthID,
Flags: SponsorshipTransferFlags.tfSponsorshipCreate,
Sponsor: sponsorA.address,
SponsorFlags: SponsorFlags.spfSponsorReserve
}
validate(createTx)
const preparedCreateTx = await client.autofill(createTx)
console.log(JSON.stringify(preparedCreateTx, null, 2))Sponsor A co-signs the transaction, so it must sign the exact Fee amount it agrees to pay. Autofill the transaction before signing so the Fee field is set; if the fee is added or changed after signing, the signature no longer matches the transaction.
// Sign as the sponsee, then co-sign as Sponsor A ----------------------
console.log(`\n=== Submitting SponsorshipTransfer transaction... ===`)
const createSignedTx = signAsSponsor(sponsorA, sponsee.sign(preparedCreateTx).tx_blob)
const createResponse = await client.submitAndWait(createSignedTx.tx_blob)
if (createResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = createResponse.result.meta.TransactionResult
console.error('Error: Unable to start the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
let fields = createResponse.result.meta.AffectedNodes.find(
node => node.ModifiedNode?.LedgerEntryType === 'DepositPreauth'
).ModifiedNode.FinalFields
console.log('Sponsorship started successfully!')
console.log(`DepositPreauth reserve now sponsored by: ${fields.Sponsor}`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${createResponse.result.hash}`)To move the reserve sponsorship from Sponsor A to Sponsor B, submit a SponsorshipTransfer transaction with the tfSponsorshipReassign flag enabled, the entry's ID, and Sponsor B's address in the Sponsor field.
// Prepare SponsorshipTransfer transaction to reassign the sponsorship ----------------
// tfSponsorshipReassign moves the reserve to Sponsor B in one transaction. Only the
// incoming sponsor has to consent; Sponsor A's obligation is released automatically.
console.log(`\n=== Preparing SponsorshipTransfer transaction to reassign the sponsorship... ===`)
const reassignTx = {
TransactionType: 'SponsorshipTransfer',
Account: sponsee.address,
ObjectID: preauthID,
Flags: SponsorshipTransferFlags.tfSponsorshipReassign,
Sponsor: sponsorB.address,
SponsorFlags: SponsorFlags.spfSponsorReserve
}
validate(reassignTx)
const preparedReassignTx = await client.autofill(reassignTx)
console.log(JSON.stringify(preparedReassignTx, null, 2))Sponsor B co-signs to accept the reserve obligation; Sponsor A does not need to sign.
// Sign as the sponsee, then co-sign as Sponsor B ----------------------
console.log(`\n=== Submitting SponsorshipTransfer transaction... ===`)
const reassignSignedTx = signAsSponsor(sponsorB, sponsee.sign(preparedReassignTx).tx_blob)
const reassignResponse = await client.submitAndWait(reassignSignedTx.tx_blob)
if (reassignResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = reassignResponse.result.meta.TransactionResult
console.error('Error: Unable to reassign the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
fields = reassignResponse.result.meta.AffectedNodes.find(
node => node.ModifiedNode?.LedgerEntryType === 'DepositPreauth'
).ModifiedNode.FinalFields
console.log('Sponsorship reassigned successfully!')
console.log(`DepositPreauth reserve now sponsored by: ${fields.Sponsor}`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${reassignResponse.result.hash}`)Only the sponsee can reassign a sponsorship, because the sponsee chooses which sponsor to rely on.
You can end a sponsorship by submitting a SponsorshipTransfer transaction with the tfSponsorshipEnd flag enabled. Either party can end a sponsorship, but this example has the sponsee submit the transaction. When the sponsor submits instead, it must also include the Sponsee field. If it succeeds, the Sponsor field is removed and the sponsee becomes responsible for the entry's reserve again.
// Prepare SponsorshipTransfer transaction to end the sponsorship ----------------------
// tfSponsorshipEnd takes no Sponsor field and needs no co-signature. If it
// succeeds, the Sponsor field is removed and the sponsee becomes responsible for
// the entry's reserve again.
console.log(`\n=== Preparing SponsorshipTransfer transaction to end the sponsorship... ===`)
const endTx = {
TransactionType: 'SponsorshipTransfer',
Account: sponsee.address,
ObjectID: preauthID,
Flags: SponsorshipTransferFlags.tfSponsorshipEnd
}
validate(endTx)
console.log(JSON.stringify(endTx, null, 2))Because no co-signature is involved, the sponsee signs and submits on its own.
// Submit the SponsorshipTransfer transaction to end the sponsorship ----------------------
console.log(`\n=== Submitting SponsorshipTransfer transaction... ===`)
const endResponse = await client.submitAndWait(endTx, {
wallet: sponsee,
autofill: true
})
if (endResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = endResponse.result.meta.TransactionResult
console.error('Error: Unable to end the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
fields = endResponse.result.meta.AffectedNodes.find(
node => node.ModifiedNode?.LedgerEntryType === 'DepositPreauth'
).ModifiedNode.FinalFields
if (fields.Sponsor !== undefined) {
console.error('Error: The DepositPreauth entry still has a Sponsor field')
await client.disconnect()
process.exit(1)
}
console.log('Sponsorship ended successfully!')
console.log(`The sponsee now pays the DepositPreauth entry's owner reserve again.`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${endResponse.result.hash}`)
await client.disconnect()Ending a ledger entry's sponsorship doesn't check whether the sponsee can cover the reserve it takes back. The sponsee can end up below its required reserve, which blocks it from creating new ledger entries until it funds the difference.
If the sponsored ledger entry is deleted instead, you don't need to end the sponsorship first. Deleting the entry releases the reserve obligation, which means the sponsor's SponsoringOwnerCount decreases and the reserve frees up.
Each step above passes an ObjectID to identify the DepositPreauth entry. To create, reassign, or end the sponsorship on an account's own reserve, omit ObjectID entirely: the transaction then applies to the account in the Account field. The following differences apply when the target is an account:
- The sponsor's
SponsorSignatureis required when creating or reassigning, not optional. - The counters the transaction moves are
SponsoringAccountCountrather thanSponsoringOwnerCountandSponsoredOwnerCount. - Ending the sponsorship checks that the account can hold its own account reserve afterwards. If it can't, the transaction fails with
tecINSUFFICIENT_RESERVE.
Only the entry types listed under Sponsor in the ledger entry common fields support sponsorship. Targeting any other type fails with tecNO_PERMISSION.