XRPL Accounts - Theory and Practice
Learning Objectives
Generate cryptographic key pairs using both secp256k1 and ed25519 algorithms
Understand the relationship between seeds, keys, and addresses
Activate accounts on testnet using the faucet and on mainnet through funding
Calculate reserve requirements for accounts with various owned objects
Implement secure key management practices appropriate for development and production
Here's a concept that confuses many developers new to XRPL: You can generate a valid XRPL address right now, but it won't exist on the ledger.
Unlike Ethereum, where addresses can receive tokens without any on-chain existence, XRPL requires accounts to be activated with a minimum balance before they can receive funds or participate in the network. This design choice has important implications for how you build applications.
Key insight
Generating keys is free and instant. Creating an account on the ledger costs XRP and takes a transaction.
- You can pre-generate addresses for users before they fund them
- Unfunded addresses can't receive XRP (transactions fail)
- Account creation has a cost (the reserve requirement)
- Accounts can't be easily deleted once created
Let's understand each component in detail.
XRPL uses a hierarchical key structure:
Seed (Secret)
↓
Private Key (derived from seed)
↓
Public Key (derived from private key)
↓
Account Address (derived from public key)- A 16-byte random value
- Encoded as a string starting with "s" (e.g., `sEd7...`)
- This is what you back up and protect
- Can regenerate everything else from this
- Derived deterministically from the seed
- Used to sign transactions
- Never transmitted or exposed
- Derived from the private key
- Can be shared freely
- Used to verify signatures
- Derived from the public key using RIPEMD160(SHA256(public_key))
- Starts with "r" (e.g., `rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh`)
- This is what you share to receive XRP
XRPL supports two cryptographic algorithms:
| Algorithm | Key Prefix | Signature Size | Speed | Security |
|---|---|---|---|---|
| secp256k1 | (none/default) | 72 bytes | Slower | Battle-tested |
| ed25519 | "ED" prefix | 64 bytes | Faster | Modern, preferred |
secp256k1 is the same algorithm Bitcoin uses. It's well-understood but computationally heavier.
ed25519 is a modern algorithm that's faster and has better security properties. New applications should prefer ed25519.
// src/accounts/generate-keys.js
const xrpl = require('xrpl');
// Method 1: Generate a random wallet (recommended)
function generateRandomWallet() {
// algorithm can be 'secp256k1' (default) or 'ed25519'
const wallet = xrpl.Wallet.generate('ed25519');
return {
seed: wallet.seed, // Back this up securely!
publicKey: wallet.publicKey,
privateKey: wallet.privateKey,
address: wallet.address // a.k.a. classicAddress
};
}
// Method 2: Derive wallet from existing seed
function walletFromSeed(seed) {
const wallet = xrpl.Wallet.fromSeed(seed);
return wallet;
}
// Method 3: Derive wallet from mnemonic (BIP-39)
function walletFromMnemonic(mnemonic) {
// xrpl.js supports standard BIP-39 mnemonics
const wallet = xrpl.Wallet.fromMnemonic(mnemonic);
return wallet;
}
// Demonstration
console.log('=== Generating New Wallet ===');
const newWallet = generateRandomWallet();
console.log('Address:', newWallet.address);
console.log('Seed:', newWallet.seed);
console.log('Public Key:', newWallet.publicKey);
console.log('\n⚠️ NEVER share your seed! This is for demonstration only.');
// Verify we can recreate from seed
console.log('\n=== Recreating from Seed ===');
const recreated = walletFromSeed(newWallet.seed);
console.log('Same address?', recreated.address === newWallet.address);
module.exports = { generateRandomWallet, walletFromSeed, walletFromMnemonic };
```
# src/accounts/generate_keys.py
from xrpl.wallet import Wallet
from xrpl.core.keypairs import derive_keypair, derive_classic_address
from xrpl.core.addresscodec import encode_seed, decode_seed
import secrets
def generate_random_wallet(algorithm: str = "ed25519") -> dict:
"""Generate a new random wallet.
Args:
algorithm: 'ed25519' (recommended) or 'secp256k1'
Returns:
Dictionary with seed, keys, and address
"""
wallet = Wallet.create(algorithm=algorithm)
return {
"seed": wallet.seed,
"public_key": wallet.public_key,
"private_key": wallet.private_key,
"address": wallet.classic_address
}
def wallet_from_seed(seed: str) -> Wallet:
"""Recreate wallet from seed."""
return Wallet.from_seed(seed)
def wallet_from_mnemonic(mnemonic: str) -> Wallet:
"""Create wallet from BIP-39 mnemonic."""
return Wallet.from_mnemonic(mnemonic)
Demonstration
if name == "main":
print("=== Generating New Wallet ===")
new_wallet = generate_random_wallet()
print(f"Address: {new_wallet['address']}")
print(f"Seed: {new_wallet['seed']}")
print(f"Public Key: {new_wallet['public_key']}")
print("\n⚠️ NEVER share your seed! This is for demonstration only.")
Verify we can recreate from seed
print("\n=== Recreating from Seed ===")
recreated = wallet_from_seed(new_wallet['seed'])
print(f"Same address? {recreated.classic_address == new_wallet['address']}")
XRPL requires accounts to maintain a minimum XRP balance. This serves several purposes:
- **Spam prevention**: Creating accounts has a cost
- **State bloat control**: Discourages creating unnecessary accounts
- **Network sustainability**: Ensures minimum transaction capacity
- **Base Reserve:** 10 XRP (minimum to activate an account)
- **Owner Reserve:** 2 XRP per owned object
- Trust lines (each token you enable)
- Open orders on the DEX
- Escrows
- Payment channels
- Signer lists
- And others...
// src/accounts/reserve-calculator.js
function calculateReserve(ownedObjects = 0, baseReserve = 10, ownerReserve = 2) {
return baseReserve + (ownedObjects * ownerReserve);
}
// Examples
console.log('New account (0 objects):', calculateReserve(0), 'XRP');
console.log('Account with 5 trust lines:', calculateReserve(5), 'XRP');
console.log('Account with 10 DEX orders:', calculateReserve(10), 'XRP');
console.log('Active trader (20 objects):', calculateReserve(20), 'XRP');
// Output:
// New account (0 objects): 10 XRP
// Account with 5 trust lines: 20 XRP
// Account with 10 DEX orders: 30 XRP
// Active trader (20 objects): 50 XRP
```
Important
You cannot send XRP that would drop your balance below the reserve. This "locked" XRP is often called "frozen" or "reserved" XRP.
On testnet, use the faucet to activate and fund accounts:
// src/accounts/activate-testnet.js
const xrpl = require('xrpl');
async function activateTestnetAccount() {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
// Generate a new wallet
const wallet = xrpl.Wallet.generate('ed25519');
console.log('Generated address:', wallet.address);
// Fund it using the faucet
console.log('Requesting testnet XRP from faucet...');
const fundResult = await client.fundWallet(wallet);
console.log('Funded!');
console.log('Balance:', fundResult.balance, 'XRP');
console.log('Wallet:', fundResult.wallet.address);
// Verify account exists on ledger
const accountInfo = await client.request({
command: 'account_info',
account: wallet.address
});
console.log('\nAccount now exists on ledger:');
console.log('Sequence:', accountInfo.result.account_data.Sequence);
console.log('Balance:', xrpl.dropsToXrp(accountInfo.result.account_data.Balance), 'XRP');
await client.disconnect();
return {
wallet,
balance: fundResult.balance
};
}
activateTestnetAccount().catch(console.error);
On mainnet, you must send XRP from an existing funded account:
// src/accounts/activate-mainnet.js
const xrpl = require('xrpl');
async function activateMainnetAccount(fundingWallet, newAddress, amount = '15') {
// WARNING: This uses real XRP on mainnet!
const client = new xrpl.Client('wss://xrplcluster.com');
await client.connect();
// Verify the new address isn't already funded
try {
await client.request({
command: 'account_info',
account: newAddress
});
console.log('Account already exists!');
await client.disconnect();
return;
} catch (error) {
if (error.data?.error !== 'actNotFound') {
throw error;
}
// Account doesn't exist - this is expected
}
// Create and submit funding transaction
const payment = {
TransactionType: 'Payment',
Account: fundingWallet.address,
Destination: newAddress,
Amount: xrpl.xrpToDrops(amount) // Convert XRP to drops
};
console.log(`Sending ${amount} XRP to activate ${newAddress}...`);
const prepared = await client.autofill(payment);
const signed = fundingWallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
console.log('Transaction result:', result.result.meta.TransactionResult);
await client.disconnect();
return result;
}
// DO NOT RUN THIS without a funded mainnet wallet!
// This is for reference only
Every active account has an AccountRoot ledger entry:
// Query account information
const accountInfo = await client.request({
command: 'account_info',
account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
ledger_index: 'validated'
});
// AccountRoot structure
{
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Balance": "10000000000", // Balance in drops (100 XRP)
"Flags": 0, // Account flags
"LedgerEntryType": "AccountRoot",
"OwnerCount": 0, // Number of owned objects
"PreviousTxnID": "ABC123...", // Last transaction affecting this account
"PreviousTxnLgrSeq": 1234567, // Ledger of last transaction
"Sequence": 1, // Next transaction sequence number
"index": "DEF456..." // Ledger entry index
}
Balance: Account balance in drops (1 XRP = 1,000,000 drops). Always an integer string.
Sequence: Transaction counter. Starts at 1 for new accounts and increments with each transaction. Used to order transactions and prevent replays.
OwnerCount: Number of objects this account owns (trust lines, orders, etc.). Affects reserve requirement.
Flags: Bitfield controlling account behavior (we'll cover specific flags later).
// src/accounts/query-account.js
const xrpl = require('xrpl');
async function getAccountDetails(address) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
try {
// Basic account info
const accountInfo = await client.request({
command: 'account_info',
account: address,
ledger_index: 'validated'
});
const data = accountInfo.result.account_data;
// Calculate available vs reserved balance
const balance = Number(data.Balance) / 1_000_000;
const baseReserve = 10;
const ownerReserve = data.OwnerCount * 2;
const totalReserve = baseReserve + ownerReserve;
const available = Math.max(0, balance - totalReserve);
return {
address: data.Account,
balance: balance,
reserved: totalReserve,
available: available,
sequence: data.Sequence,
ownerCount: data.OwnerCount,
flags: data.Flags
};
} catch (error) {
if (error.data?.error === 'actNotFound') {
return {
address: address,
exists: false,
message: 'Account not found (not activated)'
};
}
throw error;
} finally {
await client.disconnect();
}
}
// Example usage
async function main() {
// Try with a testnet address after running activate-testnet.js
const address = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
const details = await getAccountDetails(address);
console.log('Account Details:');
console.log(JSON.stringify(details, null, 2));
}
main().catch(console.error);
module.exports = { getAccountDetails };
```
- Match the account's current Sequence value
- Be unique (each number used exactly once)
- Be used in order (can't skip numbers)
- **Transaction replay attacks**: Same transaction can't be submitted twice
- **Out-of-order execution**: Transactions execute in a defined order
- **Stuck transactions**: Clear way to cancel pending transactions
// src/accounts/sequence-management.js
const xrpl = require('xrpl');
class SequenceManager {
constructor(client, address) {
this.client = client;
this.address = address;
this.localSequence = null;
}
async initialize() {
const info = await this.client.request({
command: 'account_info',
account: this.address
});
this.localSequence = info.result.account_data.Sequence;
return this.localSequence;
}
getNext() {
if (this.localSequence === null) {
throw new Error('SequenceManager not initialized');
}
return this.localSequence++;
}
async sync() {
// Resync with ledger (use after errors or uncertainty)
return this.initialize();
}
// For parallel transaction submission
reserveRange(count) {
const start = this.localSequence;
this.localSequence += count;
return { start, end: this.localSequence - 1 };
}
}
// Usage example
async function demonstrateSequences() {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
const wallet = xrpl.Wallet.generate();
await client.fundWallet(wallet);
const seqManager = new SequenceManager(client, wallet.address);
await seqManager.initialize();
console.log('Initial sequence:', seqManager.localSequence);
// Get sequences for 3 transactions
console.log('Transaction 1 sequence:', seqManager.getNext());
console.log('Transaction 2 sequence:', seqManager.getNext());
console.log('Transaction 3 sequence:', seqManager.getNext());
await client.disconnect();
}
module.exports = { SequenceManager };
```
Problem 1: Sequence too low
terPRE_SEQ - Sequence number is lower than current account sequence
Usually means the transaction already succeeded or you're reusing a sequence.
Problem 2: Sequence too high
terQUEUED - Transaction queued (waiting for earlier sequence)
Transactions can queue if you skip ahead, but this is risky.
- Use the SequenceManager to track locally
- Use `autofill()` carefully (it queries each time)
- Reserve sequences in advance for batch operations
Your seed (secret) can regenerate your entire wallet. Anyone with your seed controls your funds completely.
SEED SECURITY HIERARCHY:
Most Secure:
├── Hardware wallet (Ledger, Trezor)
├── Air-gapped computer generation
├── Encrypted offline storage
├── Split seed across locations
Acceptable for Development:
├── Environment variables (.env file)
├── Encrypted config files
├── macOS Keychain / Windows Credential Manager
NEVER:
├── Hardcode in source code
├── Commit to git
├── Store in plain text
├── Share via email/chat
├── Use online seed generators
// src/accounts/secure-wallet.js
require('dotenv').config();
const xrpl = require('xrpl');
function loadWalletFromEnv() {
const seed = process.env.XRPL_SEED;
if (!seed) {
throw new Error('XRPL_SEED not found in environment variables');
}
if (!seed.startsWith('s')) {
throw new Error('Invalid seed format');
}
return xrpl.Wallet.fromSeed(seed);
}
// .env file (NEVER commit this!)
// XRPL_SEED=sEdVbYm6mGqzLQ7D8WgjS7hF8bLtXMX
// .gitignore (ALWAYS include this)
// .env
// .env
// .env.
module.exports = { loadWalletFromEnv };
```
XRPL allows accounts to use a "regular key" for signing instead of the master key:
Master Key: The original key derived from the seed
Regular Key: An alternative key that can sign transactions
- Can be rotated without changing address
- Can disable master key for security
- Allows key recovery procedures
We'll cover implementing regular keys in Lesson 15 (Security Best Practices).
Yes, but with restrictions:
Account Sequence must be at least 256 higher than current ledger sequence
Account must own no objects (OwnerCount = 0)
Must specify a destination for remaining XRP
Transaction fee is higher (currently 5 XRP equivalent)
Destination account must already exist
Balance minus deletion fee
NOT the base reserve (that's burned)
// src/accounts/delete-account.js
const xrpl = require('xrpl');
async function deleteAccount(wallet, destinationAddress) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
// Check deletion eligibility
const accountInfo = await client.request({
command: 'account_info',
account: wallet.address
});
const serverInfo = await client.request({ command: 'server_info' });
const currentLedgerSeq = serverInfo.result.info.validated_ledger.seq;
const accountSeq = accountInfo.result.account_data.Sequence;
const ownerCount = accountInfo.result.account_data.OwnerCount;
// Check requirements
if (ownerCount > 0) {
throw new Error(Cannot delete: account owns ${ownerCount} objects);
}
if (accountSeq < currentLedgerSeq + 256) {
const needed = currentLedgerSeq + 256 - accountSeq;
throw new Error(Need ${needed} more transactions before deletion eligible);
}
// Prepare AccountDelete transaction
const deleteTransaction = {
TransactionType: 'AccountDelete',
Account: wallet.address,
Destination: destinationAddress
};
const prepared = await client.autofill(deleteTransaction);
const signed = wallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
console.log('Account deletion result:', result.result.meta.TransactionResult);
await client.disconnect();
return result;
}
// Note: Most accounts won't meet the sequence requirement
// This is mainly useful for cleaning up test accounts
```
- The sequence requirement (256 transactions minimum) is hard to meet
- You lose the base reserve regardless
- Better to just stop using the account
- Cleaning up after extensive testing
- Consolidating funds from many accounts
- Recovering XRP from abandoned accounts with many transactions
XRPL's account model is more complex than Ethereum's but more protective against user error (can't accidentally send to an invalid address that won't exist). The reserve requirement, while sometimes frustrating, prevents spam and keeps the ledger manageable. Treat your seed like cash—once it's gone, it's gone.
Assignment: Build a comprehensive account management script that handles creation, storage, and querying.
Requirements:
Generate wallets using ed25519 algorithm
Support recreation from seed
Include input validation
Store seeds in environment variables
Create a
.env.exampletemplate (without real secrets)Document proper
.gitignoresetupQuery account details from ledger
Calculate and display available vs reserved balance
Handle non-existent accounts gracefully
Script to generate, fund, and verify a new testnet account
Display all relevant account information after funding
Save the seed securely for future lessons
Key generation works correctly (25%)
Secure storage properly implemented (25%)
Account query handles all cases (25%)
Testnet activation successful with verification (25%)
Time investment: 2 hours
Value: These utilities will be used throughout the remainder of the course
1. Key Hierarchy Question:
Which of the following statements about XRPL key hierarchy is TRUE?
A) The address is derived from the seed using SHA-256
B) Anyone with your public key can derive your private key
C) The seed can regenerate the private key, public key, and address
D) Each account has multiple valid seeds that generate the same address
Correct Answer: C
Explanation: The key hierarchy is deterministic: seed → private key → public key → address. Given a seed, you can always derive everything below it. However, you cannot go backwards—the address cannot reveal the public key (without separate disclosure), and the public key cannot reveal the private key. Each seed generates exactly one address; there are no alternative seeds for the same address.
2. Reserve Calculation Question:
An account has 50 XRP balance and owns 15 trust lines plus 5 open DEX orders. How much XRP can this account send in a transaction?
A) 50 XRP
B) 40 XRP
C) 0 XRP
D) 10 XRP
Correct Answer: C
Explanation: Reserve calculation: Base reserve (10 XRP) + Owner reserve (20 objects × 2 XRP = 40 XRP) = 50 XRP total reserve. With a 50 XRP balance and 50 XRP reserve, the available balance is 0 XRP. The account cannot send any XRP until it reduces owned objects or receives more XRP. This is a common trap for active traders.
3. Account Activation Question:
You generate a new XRPL address and give it to a customer to receive payment. The customer sends 5 XRP. What happens?
A) The payment succeeds and the new account is created with 5 XRP
B) The payment fails because 5 XRP is below the 10 XRP minimum
C) The payment succeeds but the account has 0 spendable XRP
D) The payment is queued until the account is properly activated
Correct Answer: B
Explanation: XRPL requires the minimum base reserve (10 XRP) to activate an account. A payment of less than this amount to a non-existent address fails with tecNO_DST_INSUF_XRP. The funds are not lost—they stay with the sender. This is why you should verify account activation before expecting payments, or ensure first payments are at least 10 XRP.
4. Sequence Number Question:
Your account has Sequence 5. You submit three transactions: first with Sequence 5, second with Sequence 7, third with Sequence 6. What happens?
A) All three succeed in order 5, 7, 6
B) Transaction 5 succeeds; 7 queues waiting for 6; 6 succeeds and unblocks 7
C) Transaction 5 succeeds; 6 and 7 fail immediately
D) All three fail because of sequence conflicts
Correct Answer: B
Explanation: Sequence 5 succeeds (matches account state, advancing it to 6). Sequence 7 doesn't match (account expects 6), so it queues. Sequence 6 succeeds (matches), advancing state to 7, which allows queued transaction 7 to proceed. This queuing behavior can be useful but is risky—if sequence 6 never arrives, sequence 7 stays queued until expiration.
5. Security Practice Question:
Which is the BEST practice for storing XRPL seeds during development?
A) Hardcode in configuration file committed to git for team access
B) Store in a .env file excluded from version control
C) Use the same testnet seed as mainnet for consistency
D) Store seeds in a database with other application data
Correct Answer: B
Explanation: Environment variables (via .env files excluded from git) are the appropriate balance for development: convenient enough to use but not committed to version control. Never hardcode secrets (A) or store them in databases that might be compromised (D). Always use separate seeds for testnet and mainnet (C)—testnet seeds can be treated less securely, but habits carry over.
- Account basics: https://xrpl.org/accounts.html
- Reserves: https://xrpl.org/reserves.html
- Cryptographic keys: https://xrpl.org/cryptographic-keys.html
- Account deletion: https://xrpl.org/accounts.html#deletion-of-accounts
- BIP-39 mnemonics: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
- ed25519 specification: https://ed25519.cr.yp.to/
- XRPL key security: https://xrpl.org/secure-signing.html
- Hardware wallet integration guides (vendor-specific)
For Next Lesson:
You'll need an activated testnet account with some XRP for Lesson 3. Run your account creation script and save the seed securely. Lesson 3 will use this account to send your first XRP payment.
End of Lesson 2
Total words: ~4,800
Estimated completion time: 50 minutes reading + 2 hours for deliverable
Key Takeaways
Generating keys ≠ creating an account
: Keys are free to generate; accounts must be funded to exist on-chain. Pre-generate addresses but remember they can't receive funds until activated.
Reserve requirements are real constraints
: Plan for 10 XRP base + 2 XRP per object. Users with many trust lines need significant XRP just for reserves.
Sequence numbers are your transaction ordering system
: Track them carefully, especially for concurrent transactions. Sequence errors are one of the most common bugs.
The seed is the single point of failure
: Secure it appropriately for your risk level. Hardware wallets for production, environment variables for development, never in code.
Account deletion is rarely practical
: The 256-transaction sequence requirement means most accounts should just be abandoned rather than deleted. ---