Documentation Index
Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
Overview
XRP uses the secp256k1 elliptic curve — the same as Ethereum. You recover the compressed public key from the EVM wallet, then compute RIPEMD-160(SHA-256(pubkey)) and encode with XRP’s custom base58 alphabet. Transactions use XRP’s binary serialization format and require DER-encoded signatures with low-S normalization.
| Property | Value |
|---|
| Curve | secp256k1 |
| Root Wallet | EVM |
| Address Format | base58check with Ripple alphabet (r...) |
| Hashing | SHA-256 + RIPEMD-160 (address), SHA-512/256 (signing) |
| Serialization | XRP binary format |
| Smallest Unit | Drops (1 XRP = 10^6 drops) |
Dependencies
npm install @noble/curves @noble/hashes bs58
Derive Address
Recover the secp256k1 compressed public key, compute the account ID, and encode with XRP’s base58 alphabet:
import { sha256 } from "@noble/hashes/sha2";
import { ripemd160 } from "@noble/hashes/legacy";
import bs58 from "bs58";
const BITCOIN_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const RIPPLE_ALPHABET = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"; // trufflehog:ignore
function xrpAddressFromPubkey(compressedPubkey: Uint8Array): string {
const accountId = ripemd160(sha256(compressedPubkey));
const versioned = new Uint8Array(21);
versioned[0] = 0x00;
versioned.set(accountId, 1);
const checksum = sha256(sha256(versioned)).slice(0, 4);
const full = concatBytes(versioned, checksum);
const btcEncoded = bs58.encode(full);
return btcEncoded.split("").map(c => RIPPLE_ALPHABET[BITCOIN_ALPHABET.indexOf(c)]).join("");
}
async function deriveXrpAddress(): Promise<{ address: string; publicKey: string }> {
const compressedPubkey = await recoverEvmPubkey("XRP_PUBKEY_RECOVERY");
return {
address: xrpAddressFromPubkey(compressedPubkey),
publicKey: bytesToHex(compressedPubkey),
};
}
Sign a Message
import { sha256 } from "@noble/hashes/sha2";
async function signXrpMessage(message: string): Promise<string> {
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "EVM")!;
const messageBytes = new TextEncoder().encode(message);
const prefix = new TextEncoder().encode("\x19XRP Signed Message:\n");
const lengthStr = new TextEncoder().encode(String(messageBytes.length));
const digest = sha256(concatBytes(prefix, lengthStr, messageBytes));
const signature = await dynamicClient.wallets.waas.signRawMessage(wallet.id, {
accountAddress: wallet.address,
message: bytesToHex(digest),
});
return signature;
}
Sign a Transaction
Build an XRP Payment transaction, sign with SHA-512/256, DER-encode the signature, and submit:
import { sha512 } from "@noble/hashes/sha2";
async function sendXrpTransfer(
to: string,
amount: number,
xrpAddress: string,
publicKey: string,
): Promise<string> {
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "EVM")!;
const pubkeyBytes = hexToBytes(publicKey);
const drops = BigInt(Math.round(amount * 1_000_000));
const [accountInfo, feeInfo] = await Promise.all([
xrpRpc("account_info", [{ account: xrpAddress, ledger_index: "current" }]),
xrpRpc("fee", []),
]);
const sequence = accountInfo.account_data.Sequence;
const fee = BigInt(Math.max(parseInt(feeInfo.drops?.open_ledger_fee || "12"), 12));
const lastLedger = (feeInfo.ledger_current_index || 0) + 75;
const serialized = serializePayment({
account: xrpAddressToAccountId(xrpAddress),
destination: xrpAddressToAccountId(to),
amount: drops,
fee,
sequence,
lastLedgerSequence: lastLedger,
signingPubKey: pubkeyBytes,
});
// Signing hash: SHA-512 first half of (0x53545800 + serialized)
const HASH_PREFIX = new Uint8Array([0x53, 0x54, 0x58, 0x00]);
const hash = sha512(concatBytes(HASH_PREFIX, serialized)).slice(0, 32);
const signature = await dynamicClient.wallets.waas.signRawMessage(wallet.id, {
accountAddress: wallet.address,
message: bytesToHex(hash),
});
// DER-encode the signature with low-S normalization
const sigRaw = hexToBytes(signature.replace(/^0x/, ""));
const derSig = sigToDER(bytesToHex(sigRaw.slice(0, 64)));
const signedSerialized = serializePayment({
account: xrpAddressToAccountId(xrpAddress),
destination: xrpAddressToAccountId(to),
amount: drops,
fee,
sequence,
lastLedgerSequence: lastLedger,
signingPubKey: pubkeyBytes,
txnSignature: derSig,
});
const txBlob = bytesToHex(signedSerialized).toUpperCase();
const result = await xrpRpc("submit", [{ tx_blob: txBlob }]);
if (!result.engine_result?.startsWith("tes") && !result.engine_result?.startsWith("ter")) {
throw new Error(`XRP submit failed: ${result.engine_result_message}`);
}
return result.tx_json?.hash;
}
For the XRP binary serialization helpers (serializePayment, sigToDER, xrpAddressToAccountId, xrpRpc) see the JavaScript XRP guide. The only difference in React Native is the signing call.
Verify a Signature
import { secp256k1 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha2";
function verifyXrpSignature(
message: string,
signature: string,
publicKey: string,
): boolean {
const messageBytes = new TextEncoder().encode(message);
const prefix = new TextEncoder().encode("\x19XRP Signed Message:\n");
const lengthStr = new TextEncoder().encode(String(messageBytes.length));
const digest = sha256(concatBytes(prefix, lengthStr, messageBytes));
const sigBytes = hexToBytes(signature.replace(/^0x/, ""));
const rsHex = bytesToHex(sigBytes.slice(0, 64));
let v = sigBytes[64];
if (v >= 27) v -= 27;
try {
const sig = secp256k1.Signature.fromHex(rsHex).addRecoveryBit(v);
return sig.recoverPublicKey(digest).toHex(true) === publicKey.toLowerCase();
} catch {
return false;
}
}
Check Balance
async function getXrpBalance(address: string): Promise<string> {
const res = await fetch("https://s.altnet.rippletest.net:51234", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
method: "account_info",
params: [{ account: address, ledger_index: "validated" }],
}),
});
const data = await res.json();
if (data.result?.error) return "0";
const drops = BigInt(data.result.account_data.Balance);
return (Number(drops) / 1_000_000).toString();
}