This tutorial shows you how to create and finish escrows that hold fungible tokens on the XRP Ledger. It covers two types of fungible token escrows:
- Conditional MPT escrow: An escrow holding Multi-Purpose Tokens that is released when a crypto-condition is fulfilled.
- Timed trust line token escrow: An escrow holding trust line tokens that is released after a specified time.
Though this tutorial covers these two specific scenarios, both fungible token types can be used in either conditional or timed escrows.
Requires the TokenEscrow amendment. Loading...
By the end of this tutorial, you will be able to:
- Issue an MPT with escrow support enabled.
- Create and finish a conditional escrow that holds MPTs.
- Enable trust line token escrows on an issuer account.
- Create and finish a timed escrow that holds trust line tokens.
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger.
- Have an XRP Ledger client library installed. 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.
- Go with the xrpl-go library. See Get Started Using Go for setup steps.
You can find the complete source code for this tutorial's examples 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, instantiate a client to connect to the XRPL, and fund two new accounts (Issuer and Escrow Creator). This example imports:
xrpl: Used for XRPL client connection, transaction submission, and wallet handling.five-bells-conditionandcrypto: Used to generate a crypto-condition.
// This example demonstrates how to create escrows that hold fungible tokens.
// It covers MPTs and Trust Line Tokens, and uses conditional and timed escrows.
import xrpl from 'xrpl'
import { PreimageSha256 } from 'five-bells-condition'
import { randomBytes } from 'crypto'
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()
// Fund an issuer account and an escrow creator account ----------------------
console.log(`\n=== Funding Accounts ===\n`)
const [
{ wallet: issuer },
{ wallet: creator }
] = await Promise.all([
client.fundWallet(),
client.fundWallet()
])
console.log(`Issuer: ${issuer.address}`)
console.log(`Escrow Creator: ${creator.address}`)Construct an MPTokenIssuanceCreate transaction with the tfMPTCanEscrow flag, which enables the token to be held in escrows. Then, retrieve the MPT issuance ID from the transaction result. This example creates an escrow that sends MPTs back to the original issuer. If you wanted to create an escrow for another account, the issuer would also have to set the tfMPTCanTransfer flag.
// ====== Conditional MPT Escrow ======
// Issuer creates an MPT ----------------------
console.log('\n=== Creating MPT ===\n')
const mptCreateTx = {
TransactionType: 'MPTokenIssuanceCreate',
Account: issuer.address,
MaximumAmount: '1000000',
Flags: xrpl.MPTokenIssuanceCreateFlags.tfMPTCanEscrow
}
// Validate the transaction structure before submitting
xrpl.validate(mptCreateTx)
console.log(JSON.stringify(mptCreateTx, null, 2))
// Submit, sign, and wait for validation
console.log(`\nSubmitting MPTokenIssuanceCreate transaction...`)
const mptCreateResponse = await client.submitAndWait(mptCreateTx, {
wallet: issuer,
autofill: true
})
if (mptCreateResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`MPTokenIssuanceCreate failed: ${mptCreateResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
// Extract the MPT issuance ID from the transaction result
const mptIssuanceId = mptCreateResponse.result.meta.mpt_issuance_id
console.log(`MPT created: ${mptIssuanceId}`)Before the escrow creator can hold the MPT, they must indicate their willingness to hold it with the MPTokenAuthorize transaction.
// Escrow Creator authorizes the MPT ----------------------
console.log('\n=== Escrow Creator Authorizing MPT ===\n')
const mptAuthTx = {
TransactionType: 'MPTokenAuthorize',
Account: creator.address,
MPTokenIssuanceID: mptIssuanceId
}
xrpl.validate(mptAuthTx)
console.log(JSON.stringify(mptAuthTx, null, 2))
console.log(`\nSubmitting MPTokenAuthorize transaction...`)
const mptAuthResponse = await client.submitAndWait(mptAuthTx, {
wallet: creator,
autofill: true
})
if (mptAuthResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`MPTokenAuthorize failed: ${mptAuthResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log('Escrow Creator authorized for MPT.')Send MPTs from the issuer to the escrow creator using a Payment transaction.
// Issuer sends MPTs to escrow creator ----------------------
console.log('\n=== Issuer Sending MPTs to Escrow Creator ===\n')
const mptPaymentTx = {
TransactionType: 'Payment',
Account: issuer.address,
Destination: creator.address,
Amount: {
mpt_issuance_id: mptIssuanceId,
value: '5000'
}
}
xrpl.validate(mptPaymentTx)
console.log(JSON.stringify(mptPaymentTx, null, 2))
console.log(`\nSubmitting MPT Payment transaction...`)
const mptPaymentResponse = await client.submitAndWait(mptPaymentTx, {
wallet: issuer,
autofill: true
})
if (mptPaymentResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`MPT Payment failed: ${mptPaymentResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log('Successfully sent 5000 MPTs to Escrow Creator.')Conditional escrows require a fulfillment and its corresponding condition in the format of a PREIMAGE-SHA-256 crypto-condition, represented as hexadecimal. To calculate these in the correct format, use a crypto-conditions library. Generally, you want to generate the fulfillment using at least 32 random bytes from a cryptographically secure source of randomness.
// Escrow Creator creates a conditional MPT escrow ----------------------
console.log('\n=== Creating Conditional MPT Escrow ===\n')
// Generate crypto-condition
const preimage = randomBytes(32)
const fulfillment = new PreimageSha256()
fulfillment.setPreimage(preimage)
const fulfillmentHex = fulfillment.serializeBinary().toString('hex').toUpperCase()
const conditionHex = fulfillment.getConditionBinary().toString('hex').toUpperCase()
console.log(`Condition: ${conditionHex}`)
console.log(`Fulfillment: ${fulfillmentHex}\n`)Create a conditional escrow using the generated crypto-condition. Fungible token escrows require an expiration date. This example sets an expiration time of five minutes. After creating the escrow, save the sequence number to reference it later.
// Set expiration (300 seconds from now)
const cancelAfter = new Date()
cancelAfter.setSeconds(cancelAfter.getSeconds() + 300)
const cancelAfterRippleTime = xrpl.isoTimeToRippleTime(cancelAfter.toISOString())
const mptEscrowCreateTx = {
TransactionType: 'EscrowCreate',
Account: creator.address,
Destination: issuer.address,
Amount: {
mpt_issuance_id: mptIssuanceId,
value: '1000'
},
Condition: conditionHex,
CancelAfter: cancelAfterRippleTime // Fungible token escrows require a CancelAfter time
}
xrpl.validate(mptEscrowCreateTx)
console.log(JSON.stringify(mptEscrowCreateTx, null, 2))
console.log(`\nSubmitting MPT EscrowCreate transaction...`)
const mptEscrowResponse = await client.submitAndWait(mptEscrowCreateTx, {
wallet: creator,
autofill: true
})
if (mptEscrowResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`MPT EscrowCreate failed: ${mptEscrowResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
// Save the sequence number to identify the escrow later
const mptEscrowSeq = mptEscrowResponse.result.tx_json.Sequence
console.log(`Conditional MPT escrow created. Sequence: ${mptEscrowSeq}`)Finish the escrow by providing the original condition and its matching fulfillment.
// Finish the conditional MPT escrow with the fulfillment ----------------------
console.log('\n=== Finishing Conditional MPT Escrow ===\n')
const mptEscrowFinishTx = {
TransactionType: 'EscrowFinish',
Account: creator.address,
Owner: creator.address,
OfferSequence: mptEscrowSeq,
Condition: conditionHex,
Fulfillment: fulfillmentHex
}
xrpl.validate(mptEscrowFinishTx)
console.log(JSON.stringify(mptEscrowFinishTx, null, 2))
console.log(`\nSubmitting EscrowFinish transaction...`)
const mptFinishResponse = await client.submitAndWait(mptEscrowFinishTx, {
wallet: creator,
autofill: true
})
if (mptFinishResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`MPT EscrowFinish failed: ${mptFinishResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log(`Conditional MPT escrow finished successfully: https://testnet.xrpl.org/transactions/${mptFinishResponse.result.hash}`)Token issuers enable trust line token escrows differently from MPTs. Unlike MPTs, which are escrowable at the token level, trust line tokens are escrowable at the account level. When an issuer enables the asfAllowTrustLineLocking flag on their account, all trust line tokens issued from that account are escrowable.
// ====== Timed Trust Line Token Escrow ======
// Enable trust line token escrows on the issuer ----------------------
console.log('\n=== Enabling Trust Line Token Escrows on Issuer ===\n')
const accountSetTx = {
TransactionType: 'AccountSet',
Account: issuer.address,
SetFlag: xrpl.AccountSetAsfFlags.asfAllowTrustLineLocking
}
xrpl.validate(accountSetTx)
console.log(JSON.stringify(accountSetTx, null, 2))
console.log(`\nSubmitting AccountSet transaction...`)
const accountSetResponse = await client.submitAndWait(accountSetTx, {
wallet: issuer,
autofill: true
})
if (accountSetResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`AccountSet failed: ${accountSetResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log('Trust line token escrows enabled by issuer.')Establish a trust line between the escrow creator and issuer using the TrustSet transaction. The escrow creator submits this transaction to indicate their willingness to receive the token, defining the currency and maximum amount they're willing to hold.
// Escrow Creator sets up a trust line to the issuer ----------------------
console.log('\n=== Setting Up Trust Line ===\n')
const currencyCode = 'IOU'
const trustSetTx = {
TransactionType: 'TrustSet',
Account: creator.address,
LimitAmount: {
currency: currencyCode,
issuer: issuer.address,
value: '10000000'
}
}
xrpl.validate(trustSetTx)
console.log(JSON.stringify(trustSetTx, null, 2))
console.log(`\nSubmitting TrustSet transaction...`)
const trustResponse = await client.submitAndWait(trustSetTx, {
wallet: creator,
autofill: true
})
if (trustResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`TrustSet failed: ${trustResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log(`Trust line successfully created for "${currencyCode}" tokens.`)Send IOU tokens from the issuer to the escrow creator using a Payment transaction.
// Issuer sends IOU tokens to creator ----------------------
console.log('\n=== Issuer Sending IOU Tokens to Escrow Creator ===\n')
const iouPaymentTx = {
TransactionType: 'Payment',
Account: issuer.address,
Destination: creator.address,
Amount: {
currency: currencyCode,
value: '5000',
issuer: issuer.address
}
}
xrpl.validate(iouPaymentTx)
console.log(JSON.stringify(iouPaymentTx, null, 2))
console.log(`\nSubmitting Trust Line Token payment transaction...`)
const iouPayResponse = await client.submitAndWait(iouPaymentTx, {
wallet: issuer,
autofill: true
})
if (iouPayResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`Trust Line Token payment failed: ${iouPayResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log(`Successfully sent 5000 ${currencyCode} tokens.`)To make a timed escrow, set the maturity time of the escrow, which is a timestamp formatted as seconds since the Ripple Epoch. This example sets a maturity time of ten seconds from the time the code executes. Since it is a fungible token escrow, it also sets an expiration time of five minutes. After submitting the EscrowCreate transaction, save the sequence number from the transaction result.
// Escrow Creator creates a timed trust line token escrow ----------------------
console.log('\n=== Creating Timed Trust Line Token Escrow ===\n')
const delay = 10 // seconds
const now = new Date()
const finishAfter = new Date(now.getTime() + delay * 1000)
const finishAfterRippleTime = xrpl.isoTimeToRippleTime(finishAfter.toISOString())
console.log(`Escrow will mature after: ${finishAfter.toLocaleString()}\n`)
const iouCancelAfter = new Date(now.getTime() + 300 * 1000)
const iouCancelAfterRippleTime = xrpl.isoTimeToRippleTime(iouCancelAfter.toISOString())
const iouEscrowCreateTx = {
TransactionType: 'EscrowCreate',
Account: creator.address,
Destination: issuer.address,
Amount: {
currency: currencyCode,
value: '1000',
issuer: issuer.address
},
FinishAfter: finishAfterRippleTime,
CancelAfter: iouCancelAfterRippleTime
}
xrpl.validate(iouEscrowCreateTx)
console.log(JSON.stringify(iouEscrowCreateTx, null, 2))
console.log(`\nSubmitting Trust Line Token EscrowCreate transaction...`)
const iouEscrowResponse = await client.submitAndWait(iouEscrowCreateTx, {
wallet: creator,
autofill: true
})
if (iouEscrowResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`Trust Line Token EscrowCreate failed: ${iouEscrowResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
// Save the sequence number to identify the escrow later
const iouEscrowSeq = iouEscrowResponse.result.tx_json.Sequence
console.log(`Trust Line Token escrow created. Sequence: ${iouEscrowSeq}`)Wait for the escrow to mature. Before submitting the EscrowFinish transaction, the code checks the current validated ledger to confirm the close time is after the escrow maturation time. This check ensures the escrow is matured on a validated ledger before trying to finish it.
// Wait for the escrow to mature, then finish it --------------------
console.log(`\n=== Waiting For Timed Trust Line Token Escrow to Mature ===\n`)
// Sleep function to countdown delay until escrow matures
function sleep (delayInSeconds) {
return new Promise((resolve) => setTimeout(resolve, delayInSeconds * 1000))
}
for (let i = delay; i >= 0; i--) {
process.stdout.write(`\rWaiting for escrow to mature... ${i}s remaining...`)
await sleep(1)
}
console.log('\rWaiting for escrow to mature... done. ')
// Confirm latest validated ledger close time is after the FinishAfter time
let escrowReady = false
while (!escrowReady) {
const validatedLedger = await client.request({
command: 'ledger',
ledger_index: 'validated'
})
const ledgerCloseTime = validatedLedger.result.ledger.close_time
console.log(`Latest validated ledger closed at: ${new Date(xrpl.rippleTimeToISOTime(ledgerCloseTime)).toLocaleString()}`)
if (ledgerCloseTime > finishAfterRippleTime) {
escrowReady = true
console.log('Escrow confirmed ready to finish.')
} else {
let timeDifference = finishAfterRippleTime - ledgerCloseTime
if (timeDifference === 0) { timeDifference = 1 }
console.log(`Escrow needs to wait another ${timeDifference}s.`)
await sleep(timeDifference)
}
}
// Finish the timed trust line token escrow --------------------
console.log('\n=== Finishing Timed Trust Line Token Escrow ===\n')
const iouEscrowFinishTx = {
TransactionType: 'EscrowFinish',
Account: creator.address,
Owner: creator.address,
OfferSequence: iouEscrowSeq
}
xrpl.validate(iouEscrowFinishTx)
console.log(JSON.stringify(iouEscrowFinishTx, null, 2))
console.log(`\nSubmitting EscrowFinish transaction...`)
const iouFinishResponse = await client.submitAndWait(iouEscrowFinishTx, {
wallet: creator,
autofill: true
})
if (iouFinishResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
console.error(`Trust Line Token EscrowFinish failed: ${iouFinishResponse.result.meta.TransactionResult}`)
await client.disconnect()
process.exit(1)
}
console.log(`Timed Trust Line Token escrow finished successfully: https://testnet.xrpl.org/transactions/${iouFinishResponse.result.hash}`)
await client.disconnect()Concepts:
Tutorials:
References: