XRP API

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.

Key Differences from Ethereum

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

import {
  XrpApp,
  DEFAULT_XRP_PATH,
  type XrpSignatureResult,
  type XrpCurve,
} from '@openloop/sdk-core'

Initialization

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.

Methods

Method Description
getAddress(path, curve?) Get the XRP address and the public key
signTransaction(path, txBlob, curve?) Sign a serialized transaction

getAddress

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...")
}>

Parameters

Name Type Default Description
path string — BIP44 path
curve XrpCurve 'ed25519' 'ed25519' or 'secp256k1'

The XrpCurve Type

type XrpCurve = 'ed25519' | 'secp256k1'

Default Path

DEFAULT_XRP_PATH = "44'/144'/0'/0'/0'" (Ed25519: every element hardened)

Example

// 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 Selection and the P2 Value

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.


signTransaction

Sign a serialized XRP transaction.

async signTransaction(
  path: string,
  txBlob: string,
  curve?: XrpCurve
): Promise<XrpSignatureResult>

Parameters

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

What To Pass As txBlob

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).

Return Value: XrpSignatureResult

Field Type Description
signature string Signature hex (Ed25519: 64 bytes, secp256k1: DER-encoded, variable length)

Example

// 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'
)

Errors

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).


Verifying a Signature

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 publicKey

verifyXrpTransaction(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.


xrpl.js Integration

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()

Next Steps