Lending Protocol and Single Asset Vaults are disabled on Devnet. You have two options for testing these features:
- Run
rippledin stand-alone mode and enableLendingProtocolandSingleAssetVaultin the rippled.cfg file. - Connect to the Lending Protocol-specific Devnet at
https://lend.devnet.rippletest.net:51234/.
This tutorial shows you how to withdraw assets from a single asset vault. You can withdraw by specifying either how many assets you want to receive or how many shares you want to redeem. The vault burns the necessary shares and transfers the corresponding assets to your account.
(Requires the Single Asset Vault amendment )
By the end of this tutorial, you will be able to:
- Withdraw assets from a private/public vault.
- Check the vault's state after a successful withdrawal.
- Check the depositor account's state after the withdrawal.
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger.
- Have previously deposited into a vault. See Deposit into a Vault.
- 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.
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 install xrplTo get started, import the client library and instantiate a client to connect to the XRPL.
mport xrpl from "xrpl"
// Connect to the network ----------------------
// This is a lending protocol-specific devnet. This network may be taken
// offline once the lending protocol is live on mainnet.
const client = new xrpl.Client("wss://lend.devnet.rippletest.net:51233")
await client.connect()Provide the depositor account and specify the vault details:
// Get depositor account
const depositor = xrpl.Wallet.fromSeed("sEdVSq9Zsv8vQwfivTk37bWxrvpnruf")
// The ID of the vault to withraw from
const vaultID = "6AC4EC2D775C6275D314996D6ECDD16DCB9382A29FDB769951C42192FCED76EF"
// The ID of the vault's asset (MPT Issuance)
const assetMPTIssuanceId = "0003E3B486D3DACD8BB468AB33793B9626BD894A92AB3AB4"
// The ID of the vault's share (MPT Issuance)
const shareMPTIssuanceId = "0000000152E7CD364F869E832EDB806C4A7AD8B3D0C151C5"
console.log(`Depositor address: ${depositor.address}`)
console.log(`Vault ID: ${vaultID}`)
console.log(`Asset MPT issuance ID: ${assetMPTIssuanceId}`)
console.log(`Vault share MPT issuance ID: ${shareMPTIssuanceId}`)
const withdrawAmount = "1"Before withdrawing, check the vault's current state to see its total assets and available liquidity.
// Get initial vault state ----------------------
console.log("\n=== Getting initial vault state... ===")
const initialVaultInfo = await client.request({
command: "vault_info",
vault_id: vaultID,
ledger_index: "validated"
})
console.log(`Initial vault state:`)
console.log(` Assets Total: ${initialVaultInfo.result.vault.AssetsTotal}`)
console.log(` Assets Available: ${initialVaultInfo.result.vault.AssetsAvailable}`)Verify that the depositor account has vault shares to redeem. If not, the transaction will fail with a tecINSUFFICIENT_FUNDS error.
// Check depositor's share balance ----------------------
console.log("\n=== Checking depositor's share balance... ===")
try {
const shareBalanceResult = await client.request({
command: "ledger_entry",
mptoken: {
mpt_issuance_id: shareMPTIssuanceId,
account: depositor.address
},
ledger_index: "validated"
})
const shareBalance = shareBalanceResult.result.node.MPTAmount
console.log(`Shares held: ${shareBalance}`)
} catch (error) {
if (error.data?.error === 'entryNotFound') {
console.error(`Error: The depositor doesn't hold any vault shares with ID: ${shareMPTIssuanceId}.`)
}
await client.disconnect()
process.exit(1)
}Create a VaultWithdraw transaction to withdraw assets from the vault.
// Prepare VaultWithdraw transaction ----------------------
console.log(`\n=== Preparing VaultWithdraw transaction ===`)
const vaultWithdrawTx = {
TransactionType: "VaultWithdraw",
Account: depositor.address,
VaultID: vaultID,
Amount: {
mpt_issuance_id: assetMPTIssuanceId,
value: withdrawAmount
},
// Optional: Add Destination field to send assets to a different account
// Destination: "rGg4tHPRGJfewwJkd8immCFx9uSo2GgcoY"
}
// Validate the transaction structure before submitting
xrpl.validate(vaultWithdrawTx)
console.log(JSON.stringify(vaultWithdrawTx, null, 2))The transaction defines the account requesting the withdrawal, the vault's unique identifier (VaultID), and the amount to withdraw or redeem. You can specify the Amount field in two ways:
- Asset amount: When you specify an asset amount, the vault burns the necessary shares to provide that amount.
- Share amount: When you specify a share amount, the vault converts those shares into the corresponding asset amount.
While not required, you can provide a Destination account to receive the assets; if omitted, assets go to the account specified in the Account field.
You can withdraw from a vault regardless of whether it's private or public. If you hold vault shares, you can always redeem them, even if your credentials in a private vault's permissioned domain have expired or been revoked. This prevents you from being locked out of your funds.
Submit the VaultWithdraw transaction to the XRP Ledger.
// Submit VaultWithdraw transaction ----------------------
console.log("\n=== Submitting VaultWithdraw transaction... ===")
const withdrawResult = await client.submitAndWait(vaultWithdrawTx, {
wallet: depositor,
autofill: true,
})
if (withdrawResult.result.meta.TransactionResult !== "tesSUCCESS") {
const result_code = withdrawResult.result.meta.TransactionResult
console.error("Error: Unable to withdraw from vault:", result_code)
await client.disconnect()
process.exit(1)
}
console.log("Withdrawal successful!")When the transaction succeeds:
- The vault calculates how many shares need to be burned to provide the requested asset amount.
- The vault transfers the assets from its pseudo-account to the depositor account (or the
Destinationaccount if specified).
Transfer fees are not charged on VaultWithdraw transactions.
After withdrawing, check the vault's state. You can extract this information directly from the transaction metadata.
// Extract vault state from transaction metadata ----------------------
console.log("\n=== Vault state after withdrawal ===")
const affectedNodes = withdrawResult.result.meta.AffectedNodes
const vaultNode = affectedNodes.find(
(node) => {
const modifiedNode = node.ModifiedNode || node.DeletedNode
return (
modifiedNode &&
modifiedNode.LedgerEntryType === "Vault" &&
modifiedNode.LedgerIndex === vaultID
)
}
)
if (vaultNode) {
if (vaultNode.DeletedNode) {
console.log(` Vault empty (all assets withdrawn)`)
} else {
const vaultFields = vaultNode.ModifiedNode.FinalFields
console.log(` Assets Total: ${vaultFields.AssetsTotal}`)
console.log(` Assets Available: ${vaultFields.AssetsAvailable}`)
}
}Then, check the depositor's share balance:
// Get the depositor's share balance ----------------------
console.log("\n=== Depositor's share balance ==")
const depositorShareNode = affectedNodes.find((node) => {
const modifiedNode = node.ModifiedNode || node.DeletedNode
return (
modifiedNode &&
modifiedNode.LedgerEntryType === "MPToken" &&
modifiedNode.FinalFields?.Account === depositor.address &&
modifiedNode.FinalFields?.MPTokenIssuanceID === shareMPTIssuanceId
)
})
if (depositorShareNode) {
if (depositorShareNode.DeletedNode) {
console.log(`No more shares held (redeemed all shares)`)
} else {
const shareFields = depositorShareNode.ModifiedNode.FinalFields
console.log(`Shares held: ${shareFields.MPTAmount}`)
}
}Finally, verify the correct asset amount has been received by the depositor account:
// Get the depositor's asset balance ----------------------
console.log("\n=== Depositor's asset balance ==")
const depositorAssetNode = affectedNodes.find((node) => {
const assetNode = node.ModifiedNode || node.CreatedNode
const fields = assetNode?.FinalFields || assetNode?.NewFields
return (
assetNode &&
assetNode.LedgerEntryType === "MPToken" &&
fields?.Account === depositor.address &&
fields?.MPTokenIssuanceID === assetMPTIssuanceId
)
})
if (depositorAssetNode) {
const assetNode = depositorAssetNode.ModifiedNode || depositorAssetNode.CreatedNode
const assetFields = assetNode.FinalFields || assetNode.NewFields
console.log(`Balance: ${assetFields.MPTAmount}`)
}Concepts:
Tutorials:
References: