Skip to content

feat: read an account's XRPL public key, and fix raw signing for Vault accounts - #59

Merged
pdp2121 merged 2 commits into
mainfrom
feat/custody-get-public-key
Nov 5, 2026
Merged

pdp2121 merged 2 commits into
mainfrom
feat/custody-get-public-key

Conversation

@pdp2121

@pdp2121 pdp2121 commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

High Level Overview of Change

Read an account's XRPL public key. The new api.getPublicKey looks up the Custody account that holds an address and returns its XRPL public key in the form XRPL uses (SigningPubKey: compressed secp256k1, uppercase hex). Use it wherever the key itself is needed, such as setting a regular key or adding the account to a signer list:

const publicKey = await custody.api.getPublicKey('r...', { ledgerId: 'xrpl' })
// → '02218AB4…'
  • Lookup: it finds the account with findByAddress, so it searches every domain the API user can see, and ledgerId/domainId narrow an ambiguous address.
  • Errors: AccountNotFoundError for an unknown address, and a clear error when the account has no key on file yet (e.g. still provisioning).

Fix: raw signing read the wrong key field, in the wrong form. The raw-signing path (allowRawSigning) stamps the account's key into the signing preimage as SigningPubKey. It had three problems:

  • Deprecated field: it read providerDetails.keyInformation.publicKey, which the spec marks deprecated and which current Vault accounts leave empty. The sandbox primary has none, so raw signing failed there with "no exportable signing public key".
  • Wrong format: it hex-encoded the value as-is. Custody sends the key as SPKI DER, which must be compressed before XRPL can use it.
  • Wrong rejection: it refused ExtendedPublicKey entries as non-exportable. For Vault accounts, that entry's value is the account's signing key.

The fix: one shared reader in submission/account-key.ts serves both getPublicKey and raw signing.

  • It reads the account's SECP256K1_CUSTODY_1 key from providerDetails.keys, and falls back to the deprecated field on older Custody versions that don't populate keys.
  • One converter (toXrplPublicKey) compresses an SPKI key or a raw uncompressed point, and hex-encodes an already-compressed key unchanged, so older instances behave as before.
  • buildSigningPreimage now takes the XRPL-form key directly.

Docs. The README's Custody API section covers getPublicKey.

Context of Change

Reading an account's key is part of what an integration needs around Custody accounts, for example to configure regular keys or signer lists. Building it showed that the raw-signing path's key source no longer matched current Custody accounts.

This was checked against the sandbox before changing anything. The primary account has no keyInformation.publicKey. Its SECP256K1_CUSTODY_1 key, once compressed, is exactly the SigningPubKey the XRPL testnet recorded on that account's own transactions. No live test exercised the raw path, which is how the mismatch went unnoticed.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)
  • Documentation Updates
  • Release

Test Plan

  • Unit, conversion: an SPKI key is compressed correctly across 16 random keys (both 02 and 03 prefixes appear), checked against an independent implementation (@noble/curves). A raw uncompressed point is compressed, and an already-compressed key passes through unchanged.
  • Unit, key lookup:
    • the SECP256K1_CUSTODY_1 entry is read from keys and other key ids are ignored;
    • keys wins over the deprecated field;
    • an instance without keys falls back to keyInformation;
    • an account with no key returns nothing, and raw signing then throws SignerCapabilityError as before.
  • Unit, API: getPublicKey resolves the account and returns the compressed key, and throws for an unknown address, an account with no key, and an address on several ledgers. The raw-signing preimage test still reproduces exactly what Wallet.sign() computes.
  • Contract (live sandbox): getPublicKey on the primary returns a valid compressed key that derives back to the primary's address (deriveAddress).
  • Not covered live: raw signing end to end, which would submit a raw-signed transaction to testnet. The new contract test covers the key reader it now shares.

@pdp2121
pdp2121 merged commit 19dec17 into main Nov 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant