# Delete an Account This tutorial shows how to delete an [account](/docs/concepts/accounts) from the XRP Ledger, including checking that it meets the [requirements for deletion](/docs/concepts/accounts/deleting-accounts). ## Goals By following this tutorial, you should learn how to: - Check if an account can be deleted. - Delete an account. ## Prerequisites To complete this tutorial, you should: - Have a basic understanding of the XRP Ledger. - Have an XRP Ledger [client library](/docs/references/client-libraries), such as **xrpl.js**, installed. - Have an XRP Ledger Testnet account to delete. If you create a new account as part of the tutorial, you must wait about 15 minutes for it to become eligible for deletion. - Know an address where you want to send the deleted account's remaining XRP. For this tutorial, you can use the address `rJjHYTCPpNA3qAM8ZpCDtip3a8xg7B8PFo` to return funds to the Testnet faucet. ## Steps ### 1. Install dependencies JavaScript From the code sample folder, use `npm` to install dependencies: ```sh npm i ``` Python From the code sample folder, set up a virtual environment and use `pip` to install dependencies: ```sh python -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` ### 2. Connect and get accounts To get started, import the client library and instantiate an API client. To delete an account, you need the address of an account to receive the deleted account's remaining XRP. JavaScript The sample code also imports `dotenv` so that it can load environment variables from a `.env` file. import { Client, Wallet, getBalanceChanges, validate } from 'xrpl' import 'dotenv/config' const client = new Client('wss://s.altnet.rippletest.net:51233') await client.connect() // Where to send the deleted account's remaining XRP: const DESTINATION_ACCOUNT = 'rJjHYTCPpNA3qAM8ZpCDtip3a8xg7B8PFo' // Testnet faucet Python The sample code also imports `python-dotenv` so that it can load environment variables from a `.env` file. import os import json from dotenv import load_dotenv from xrpl.clients import JsonRpcClient from xrpl.wallet import Wallet, generate_faucet_wallet from xrpl.models.requests import AccountInfo, ServerState, AccountObjects from xrpl.models.transactions import AccountDelete from xrpl.transaction import submit_and_wait from xrpl.utils import get_balance_changes client = JsonRpcClient("https://s.altnet.rippletest.net:51234") # Where to send the deleted account's remaining XRP: DESTINATION_ACCOUNT = "rJjHYTCPpNA3qAM8ZpCDtip3a8xg7B8PFo" # Testnet faucet You need to instantiate a wallet instance for the account you want to delete. Since you can only delete an account that is at least ~15 minutes old, the sample code loads a seed value from a `.env` file. If you don't have an account seed defined in the `.env` file, you can get a new account from the faucet, but it won't be possible to delete it right away. JavaScript // Load the account to delete from .env file ----------------------------------- // If the seed value is still the default, get a new account from the faucet. // It won't be deletable immediately. let wallet if (!process.env.ACCOUNT_SEED || process.env.ACCOUNT_SEED === 's████████████████████████████') { console.log("Couldn't load seed from .env; getting account from the faucet.") wallet = (await client.fundWallet()).wallet console.log(`Got new account from faucet: Address: ${wallet.address} Seed: ${wallet.seed} `) console.log('Edit the .env file to add this seed, then wait until the account can be deleted.') } else { wallet = Wallet.fromSeed(process.env.ACCOUNT_SEED, { algorithm: process.env.ACCOUNT_ALGORITHM }) console.log(`Loaded account: ${wallet.address}`) } Python # Load the account to delete from .env file ----------------------------------- # If the seed value is still the default, get a new account from the faucet. # It won't be deletable immediately. load_dotenv() account_seed = os.getenv("ACCOUNT_SEED") account_algorithm = os.getenv("ACCOUNT_ALGORITHM", "ed25519") if account_seed == "s████████████████████████████" or not account_seed: print("Couldn't load seed from .env; getting account from the faucet.") wallet = generate_faucet_wallet(client) print( f"Got new account from faucet:\n" f" Address: {wallet.address}\n" f" Seed: {wallet.seed}\n" ) print( "Edit the .env file to add this seed, then wait until the account can be deleted." ) else: wallet = Wallet.from_seed(account_seed, algorithm=account_algorithm) print(f"Loaded account: {wallet.address}") ### 3. Check to see if the account can be deleted Before deleting an account, you should check that it meets the requirements for deletion. #### 3.1. Get account info The first step to checking if an account can be deleted is to get its account info as of the latest validated ledger. JavaScript // Check account info to see if account can be deleted ------------------------- let acctInfoResp try { acctInfoResp = await client.request({ command: 'account_info', account: wallet.address, ledger_index: 'validated' }) } catch (err) { console.error('account_info failed with error:', err) client.disconnect() process.exit(1) } let numProblems = 0 Python # Check account info to see if account can be deleted ------------------------- try: acct_info_resp = client.request( AccountInfo(account=wallet.address, ledger_index="validated") ) except Exception as err: print(f"account_info failed with error: {err}") exit(1) acct_info_result = acct_info_resp.result num_problems = 0 #### 3.2. Check sequence number Check that the account's current sequence number, in the `Sequence` field of the account data, is low enough compared with the latest validated ledger index. For the account to be deletable, its sequence number plus 255 must be lower than or equal to the ledger index. JavaScript // Check if sequence number is too high const acctSeq = acctInfoResp.result.account_data.Sequence const lastValidatedLedgerIndex = acctInfoResp.result.ledger_index if (acctSeq + 255 > lastValidatedLedgerIndex) { console.error(`Account is too new to be deleted. Account sequence + 255: ${acctSeq + 255} Validated ledger index: ${lastValidatedLedgerIndex} (Sequence + 255 must be less than or equal to the ledger index)`) // Estimate time until deletability assuming ledgers close every ~3.5 seconds const estWaitTimeS = (acctSeq + 255 - lastValidatedLedgerIndex) * 3.5 if (estWaitTimeS < 120) { console.log(`Estimate: ${estWaitTimeS} seconds until account can be deleted`) } else { const estWaitTimeM = Math.round(estWaitTimeS / 60, 0) console.log(`Estimate: ${estWaitTimeM} minutes until account can be deleted`) } numProblems += 1 } else { console.log(`OK: Account sequence number (${acctSeq}) is low enough.`) } Python # Check if sequence number is too high acct_seq = acct_info_result["account_data"]["Sequence"] last_validated_ledger_index = acct_info_result["ledger_index"] if acct_seq + 255 > last_validated_ledger_index: print( f"Account is too new to be deleted.\n" f" Account sequence + 255: {acct_seq + 255}\n" f" Validated ledger index: {last_validated_ledger_index}\n" f" (Sequence + 255 must be less than or equal to the ledger index)" ) # Estimate time until deletability assuming ledgers close every ~3.5 seconds est_wait_time_s = (acct_seq + 255 - last_validated_ledger_index) * 3.5 if est_wait_time_s < 120: print(f"Estimate: {est_wait_time_s} seconds until account can be deleted") else: est_wait_time_m = round(est_wait_time_s / 60) print(f"Estimate: {est_wait_time_m} minutes until account can be deleted") num_problems += 1 else: print(f"OK: Account sequence number ({acct_seq}) is low enough.") #### 3.3. Check owner count Check the `OwnerCount` field of the account data to see if the account owns too many other ledger entries. For an account to be deletable, it must own 1000 or fewer entries (of any type) in the ledger. JavaScript // Check if owner count is too high const ownerCount = acctInfoResp.result.account_data.OwnerCount if (ownerCount > 1000) { console.error(`Account owns too many objects in the ledger. Owner count: ${ownerCount} (Must be 1000 or less)`) numProblems += 1 } else { console.log(`OK: Account owner count (${ownerCount}) is low enough.`) } Python # Check if owner count is too high owner_count = acct_info_result["account_data"]["OwnerCount"] if owner_count > 1000: print( f"Account owns too many objects in the ledger.\n" f" Owner count: {owner_count}\n" f" (Must be 1000 or less)" ) num_problems += 1 else: print(f"OK: Account owner count ({owner_count}) is low enough.") #### 3.4. Check XRP balance Deleting an account requires a special [transaction cost](/docs/concepts/transactions/transaction-cost) equal to the incremental owner reserve, so an account can't be deleted if its current XRP balance is less than that. To check if an account has enough XRP, use the [server_state method](/docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_state) to look up the current incremental reserve and compare with the account's XRP balance in the `Balance` field of the account data. The [server_info method](/docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_info) returns reserve values as decimal XRP, whereas [server_state](/docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_state) returns drops of XRP. An account's `Balance` field is in drops of XRP. Be sure to compare equivalent units! (1 XRP = 1 million drops) JavaScript // Check if XRP balance is high enough // Look up current incremental owner reserve to compare vs account's XRP balance // using server_state so that both are in drops let serverStateResp try { serverStateResp = await client.request({ command: 'server_state' }) } catch (err) { console.error('server_state failed with error:', err) client.disconnect() process.exit(1) } const deletionCost = serverStateResp.result.state.validated_ledger?.reserve_inc if (!deletionCost) { console.error("Couldn't get reserve values from server. " + "Maybe it's not synced to the network?") client.disconnect() process.exit(1) } const acctBalance = acctInfoResp.result.account_data.Balance if (acctBalance < deletionCost) { console.error(`Account does not have enough XRP to pay the cost of deletion. Balance: ${acctBalance} Cost of account deletion: ${deletionCost}`) numProblems += 1 } else { console.log(`OK: Account balance (${acctBalance} drops) is high enough.`) } Python # Check if XRP balance is high enough # Look up current incremental owner reserve to compare vs account's XRP balance # using server_state so that both are in drops try: server_state_resp = client.request(ServerState()) except Exception as err: print("server_state failed with error:", err) exit(1) validated_ledger = server_state_resp.result["state"].get("validated_ledger", {}) deletion_cost = validated_ledger.get("reserve_inc") if not deletion_cost: print( "Couldn't get reserve values from server. Maybe it's not synced to the network?" ) print(json.dumps(server_state_resp.result, indent=2)) exit(1) acct_balance = int(acct_info_result["account_data"]["Balance"]) if acct_balance < deletion_cost: print( f"Account does not have enough XRP to pay the cost of deletion.\n" f" Balance: {acct_balance}\n" f" Cost of account deletion: {deletion_cost}" ) num_problems += 1 else: print(f"OK: Account balance ({acct_balance} drops) is high enough.") #### 3.5. Check NFT sequence number Check the `FirstNFTokenSequence` and `MintedNFTokens` fields of the account. (If either field is absent, you can treat its value as `0` for this purpose.) For the account to be deletable, the sum of these two numbers plus 255 must be lower than or equal to the current ledger index. JavaScript // Check if FirstNFTSequence is too high const firstNFTSeq = acctInfoResp.result.account_data.FirstNFTokenSequence || 0 const mintedNFTs = acctInfoResp.result.account_data.MintedNFTokens || 0 if (firstNFTSeq + mintedNFTs + 255 > lastValidatedLedgerIndex) { console.error(`Account's FirstNFTokenSequence + MintedNFTokens + 255 is too high. Current total: ${firstNFTSeq + mintedNFTs + 255} Validated ledger index: ${lastValidatedLedgerIndex} (FirstNFTokenSequence + MintedNFTokens + 255 must be less than or equal to the ledger index)`) numProblems += 1 } else { console.log('OK: FirstNFTokenSequence + MintedNFTokens is low enough.') } Python # Check if FirstNFTSequence is too high first_nfq_seq = acct_info_result["account_data"].get("FirstNFTokenSequence", 0) minted_nfts = acct_info_result["account_data"].get("MintedNFTokens", 0) if first_nfq_seq + minted_nfts + 255 > last_validated_ledger_index: print(f"""Account's FirstNFTokenSequence + MintedNFTokens + 255 is too high. Current total: {first_nfq_seq + minted_nfts + 255} Validated ledger index: {last_validated_ledger_index} (FirstNFTokenSequence + MintedNFTokens + 255 must be less than or equal to the the ledger index)""") num_problems += 1 else: print("OK: FirstNFTokenSequence + MintedNFTokens is low enough.") #### 3.6. Check that all issued NFTs have been burned If the account has issued any NFTs that are still present in the ledger, the account cannot be deleted. You can check to see if any exist by comparing the `MintedNFTokens` field and `BurnedNFTokens` fields of the account. (In both cases, if the field is omitted, treat its value as `0`.) If the `MintedNFTokens` value is larger, the account has issued at least one NFT that has not been burned yet. JavaScript // Check that all issued NFTs have been burned const burnedNFTs = acctInfoResp.result.account_data.BurnedNFTokens || 0 if (mintedNFTs > burnedNFTs) { console.error(`Account has issued NFTs outstanding. Number of NFTs minted: ${mintedNFTs} Number of NFTs burned: ${burnedNFTs}`) numProblems += 1 } else { console.log('OK: No outstanding, un-burned NFTs') } Python # Check that all issued NFTs have been burned burned_nfts = acct_info_result["account_data"].get("BurnedNFTokens", 0) if minted_nfts > burned_nfts: print(f"""Account has NFTs outstanding. Number of NFTs minted: {minted_nfts} Number of NFTs burned: {burned_nfts}""") num_problems += 1 else: print("OK: No outstanding, un-burned NFTs") #### 3.7. Stop if the account can't be deleted If any of the previous checks failed, you can't delete the account. Resolving the problems varies by type: - If the account sequence or NFT sequence number is too low, wait for the ledger index to advance automatically and try again later. About 15 minutes should be enough. - If the account owns too many objects, or has issued NFTs outstanding, remove the offending objects. - If the account does not have enough XRP, you can send XRP to it so that it can pay the deletion cost, but of course in this case the account does not have enough XRP for you to recover any by deleting it. You can also wait and try again if the network [votes to lower the reserve requirements](/docs/concepts/consensus-protocol/fee-voting), which would also lower the cost to delete an account. The sample code does not try to handle these problems, and quits if any problems were found: JavaScript // Stop if any problems were found if (numProblems) { console.error(`A total of ${numProblems} problem(s) prevent the account from being deleted.`) client.disconnect() process.exit(1) } Python # Stop if any problems were found if num_problems: print( f"A total of {num_problems} problem(s) prevent the account from being deleted." ) exit(1) ### 4. Check for deletion blockers and remove them if possible Some types of ledger entries can block an account from being deleted. You can check for these types of entries using the [account_objects method](/docs/references/http-websocket-apis/public-api-methods/account-methods/account_objects) with the `"deletion_blockers_only": true` parameter. JavaScript // Check for deletion blockers ------------------------------------------------- const blockers = [] let marker const ledger_index = 'validated' while (true) { let accountObjResp try { accountObjResp = await client.request({ command: 'account_objects', account: wallet.address, deletion_blockers_only: true, ledger_index, marker }) } catch (err) { console.error('account_objects failed with error:', err) client.disconnect() process.exit(1) } for (const obj of accountObjResp.result.account_objects) { blockers.push(obj) } if (accountObjResp.result.marker) { marker = accountObjResp.result.marker } else { break } } if (!blockers.length) { console.log('OK: Account has no deletion blockers.') } else { console.log(`Account cannot be deleted until ${blockers.length} blocker(s) are removed:`) for (const blocker of blockers) { console.log(JSON.stringify(blocker, null, 2)) } client.disconnect() process.exit(1) } Python # Check for deletion blockers ------------------------------------------------- blockers = [] marker = None ledger_index = "validated" while True: try: account_obj_resp = client.request( AccountObjects( account=wallet.address, deletion_blockers_only=True, ledger_index=ledger_index, marker=marker, ) ) except Exception as err: print(f"account_objects failed with error: {err}") exit(1) blockers.extend(account_obj_resp.result["account_objects"]) marker = account_obj_resp.result.get("marker") if not marker: break if not blockers: print("OK: Account has no deletion blockers.") else: print(f"Account cannot be deleted until {len(blockers)} blocker(s) are removed:") for blocker in blockers: print(json.dumps(blocker, indent=2)) exit(1) If the account has deletion blockers, you may or may not be able to remove them by sending other transactions, depending on the ledger entry. For example, if one of the blockers is a `RippleState` entry, you may be able to remove it by reducing your balance to zero through payments or offers and using a [TrustSet transaction](/docs/references/protocol/transactions/types/trustset) to return your settings to the default state. Since there are many possibilities, the sample code does not show how to remove deletion blockers. ### 5. Delete the account If all the checks passed, send an [AccountDelete transaction](/docs/references/protocol/transactions/types/accountdelete) to delete the account. Since this transaction type requires a much higher [transaction cost](/docs/concepts/transactions/transaction-cost) than normal, it's a good idea to submit the transaction with the "fail hard" setting enabled. This can save you from paying the transaction cost if the transaction was going to fail with a [`tec` result code](/docs/references/protocol/transactions/transaction-results/tec-codes). (Fail hard stops the server from relaying the transaction to the network if the transaction provisionally fails, which catches common errors before the transaction can achieve a consensus.) JavaScript // Delete the account ---------------------------------------------------------- const accountDeleteTx = { TransactionType: 'AccountDelete', Account: wallet.address, Destination: DESTINATION_ACCOUNT } validate(accountDeleteTx) console.log('Signing and submitting the AccountDelete transaction:', JSON.stringify(accountDeleteTx, null, 2)) const deleteTxResponse = await client.submitAndWait(accountDeleteTx, { wallet, autofill: true, failHard: true }) If the transaction is successful, you can use [getBalanceChanges(...)](https://js.xrpl.org/functions/getBalanceChanges.html) to check the metadata and see how much XRP was delivered from the deleted account to the destination account. // Check result of the AccountDelete transaction ------------------------------- console.log(JSON.stringify(deleteTxResponse.result, null, 2)) const resultCode = deleteTxResponse.result.meta.TransactionResult if (resultCode !== 'tesSUCCESS') { console.error(`AccountDelete failed with code ${resultCode}.`) client.disconnect() process.exit(1) } console.log('Account deleted successfully.') const balanceChanges = getBalanceChanges(deleteTxResponse.result.meta) console.log('Balance changes:', JSON.stringify(balanceChanges, null, 2)) client.disconnect() Python # Delete the account ---------------------------------------------------------- account_delete_tx = AccountDelete( account=wallet.address, destination=DESTINATION_ACCOUNT ) print("Signing and submitting the AccountDelete transaction:") print(json.dumps(account_delete_tx.to_xrpl(), indent=2)) delete_tx_response = submit_and_wait(account_delete_tx, client, wallet, fail_hard=True) If the transaction is successful, you can use [get_balance_changes(...)](https://xrpl-py.readthedocs.io/en/stable/source/xrpl.utils.html#xrpl.utils.get_balance_changes) to check the metadata and see how much XRP was delivered from the deleted account to the destination account. # Check result of the AccountDelete transaction ------------------------------- print(json.dumps(delete_tx_response.result, indent=2)) result_code = delete_tx_response.result["meta"]["TransactionResult"] if result_code != "tesSUCCESS": print(f"AccountDelete failed with code {result_code}.") exit(1) print("Account deleted successfully.") balance_changes = get_balance_changes(delete_tx_response.result["meta"]) print("Balance changes:", json.dumps(balance_changes, indent=2)) ## See Also - **Concepts:** - [Deleting Accounts](/docs/concepts/accounts/deleting-accounts) - [Transaction Cost](/docs/concepts/transactions/transaction-cost) - **References:** - [AccountDelete transaction](/docs/references/protocol/transactions/types/accountdelete) - [account_info method](/docs/references/http-websocket-apis/public-api-methods/account-methods/account_info) - [account_objects method](/docs/references/http-websocket-apis/public-api-methods/account-methods/account_objects) - [server_state method](/docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_state)