The XrpApp class provides XRP Ledger address retrieval
and transaction signing. It supports both the Ed25519 (default) and
secp256k1 elliptic curves.
XrpApp provides only these two methods. It does not
build transactions (serializing Payment, TrustSet and the other
transaction types is not part of it). Build the transaction with a
library such as xrpl.js, then pass the
unsigned, serialized transaction to the device.
| Aspect | Ethereum | XRP |
|---|---|---|
| Address format | 0x... (hex) |
r... (Base58Check) |
| Default curve | secp256k1 | Ed25519 |
| Ed25519 public key | — | Carries a 0xED prefix |
| Signature format | v/r/s | raw bytes (Ed25519: 64B, secp256k1: DER) |
| BIP44 path | 44'/60'/... |
44'/144'/... (Ed25519: fully hardened) |
import {
XrpApp,
DEFAULT_XRP_PATH,
type XrpSignatureResult,
type XrpCurve,
} from '@openloop/sdk-core'const xrp = new XrpApp(transport)Both methods take exclusive hold of the device with
transport.lock() while they run, and release it with
transport.unlock() when they finish — also when they
fail.
| Method | Description |
|---|---|
getAddress(path, curve?) |
Get the XRP address and the public key |
signTransaction(path, txBlob, curve?) |
Sign a serialized transaction |
Get the XRP Ledger address from the device.
async getAddress(
path: string,
curve?: XrpCurve
): Promise<{
publicKey: string // Public key (hex, Ed25519: 33B with 0xED prefix, secp256k1: 33B compressed)
address: string // XRP address ("r...")
}>| Name | Type | Default | Description |
|---|---|---|---|
path |
string |
— | BIP44 path |
curve |
XrpCurve |
'ed25519' |
'ed25519' or 'secp256k1' |
XrpCurve Typetype XrpCurve = 'ed25519' | 'secp256k1'DEFAULT_XRP_PATH = "44'/144'/0'/0'/0'" (Ed25519: every
element hardened)
// Ed25519 (default)
const { address, publicKey } = await xrp.getAddress(DEFAULT_XRP_PATH)
// address: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"
// publicKey: "ed01a2b3c4..." (0xED prefix)
// secp256k1
const { address: addr2, publicKey: pub2 } = await xrp.getAddress(
"44'/144'/0'/0/0", // secp256k1 also accepts a non-hardened path
'secp256k1'
)| Curve | P2 value | Description |
|---|---|---|
'ed25519' |
0x80 |
Ed25519 (default, recommended) |
'secp256k1' |
0x40 |
secp256k1 (Ethereum compatible) |
The APDU is CLA 0xE0 / INS 0x02 /
P1 0x00, and its data is the BIP32 path. The response has
the form
public key length (1B) + public key + address length (1B) + address (ASCII);
the SDK splits it up and returns the parts.
Sign a serialized XRP transaction.
async signTransaction(
path: string,
txBlob: string,
curve?: XrpCurve
): Promise<XrpSignatureResult>| Name | Type | Description |
|---|---|---|
path |
string |
BIP44 path |
txBlob |
string |
Serialized transaction (hex, with or without 0x) |
curve |
XrpCurve |
Curve ('ed25519' when omitted). Sent in P2 as the
curveMask (secp256k1=0x40 / ed25519=0x80) following the
Ledger-compatible spec. Keep it the same as the curve you used for
getAddress |
txBlobencode()
returns). Do not include TxnSignature.SigningPubKey. Put the
publicKey that getAddress returned into
SigningPubKey as is. The device determines the curve from
the first byte of SigningPubKey (0xED =
Ed25519, 0x02 / 0x03 = secp256k1).STX\0 =
53545800). The device adds it.Account must be this device’s address, derived from
path and curve. The device rejects a blob for
any other address, and it comes back as an ApduError
(0x6985). Use the same path and
curve that you passed to getAddress.A blob may be longer than 255 bytes. The SDK splits it across several
APDUs automatically (P1 0x80 for the first,
P1 0x81 for the middle ones, P1 0x01 for the
last; P1 0x00 when it fits in one).
XrpSignatureResult| Field | Type | Description |
|---|---|---|
signature |
string |
Signature hex (Ed25519: 64 bytes, secp256k1: DER-encoded, variable length) |
// Ed25519 (the default when omitted)
const { signature } = await xrp.signTransaction(
DEFAULT_XRP_PATH,
'120000228...' // unsigned transaction blob (including SigningPubKey)
)
// secp256k1 (pass the same value when you fetched the address with 'secp256k1')
const { signature: sig2 } = await xrp.signTransaction(
"44'/144'/0'/0/0",
'120000228...',
'secp256k1'
)A rejection or a busy device arrives as an ApduError
(0x6985), and a communication failure as a
TransportError. See Error
Handling for details. This call signs, so do not resend it
automatically after a timeout or
disconnected.
XrpApp cannot be used over WalletConnect (XRP is not
supported over WalletConnect).
You can verify the signature the device returned from outside the
device with verifyXrpTransaction from
@openloop/sdk-core/verify.
import { verifyXrpTransaction } from '@openloop/sdk-core/verify'
const { publicKey } = await xrp.getAddress(DEFAULT_XRP_PATH)
const result = await xrp.signTransaction(DEFAULT_XRP_PATH, txBlob)
// txBlob is the same one you passed to signTransaction (no prefix)
const check = verifyXrpTransaction(txBlob, 'ed25519', result, publicKey)
console.log(check.ok) // true means the signature belongs to publicKeyverifyXrpTransaction(txBlobHex, curve, result, pubkeyHex)
returns
{ recovered: string; expected?: string; ok: boolean }. To
verify from a fully signed transaction, use
verifyXrpSignedTx(signedTxHex); it extracts
SigningPubKey and TxnSignature itself, so you
do not pass a public key.
import { Client, encode } from 'xrpl'
import { XrpApp, DEFAULT_XRP_PATH } from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
const transport = await WebHidTransport.connect()
const xrp = new XrpApp(transport)
// Get the address
const { address, publicKey } = await xrp.getAddress(DEFAULT_XRP_PATH)
// Connect to the XRP Ledger
const client = new Client('wss://xrplcluster.com/')
await client.connect()
// Build the transaction
const prepared = await client.autofill({
TransactionType: 'Payment',
Account: address,
Amount: '1000000', // 1 XRP = 1,000,000 drops
Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
})
// Add SigningPubKey, serialize, and sign on the device
const unsigned = { ...prepared, SigningPubKey: publicKey.toUpperCase() }
const txBlob = encode(unsigned)
const { signature } = await xrp.signTransaction(DEFAULT_XRP_PATH, txBlob)
// Attach the signature to the transaction
const signedTx = {
...unsigned,
TxnSignature: signature.toUpperCase(),
}
// Broadcast
const result = await client.submitAndWait(encode(signedTx))
console.log('TX Result:', result.result.meta)
await client.disconnect()