Skip to content

Add XRPL TypeScript vault depositor#6

Open
ihsraham wants to merge 3 commits into
masterfrom
feat/xrpl-ts-depositor
Open

Add XRPL TypeScript vault depositor#6
ihsraham wants to merge 3 commits into
masterfrom
feat/xrpl-ts-depositor

Conversation

@ihsraham

@ihsraham ihsraham commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds XRPL support to the TypeScript SDK vault depositor surface.

This includes:

  • XrplVaultDepositor exported from @yellow-org/clearnet-sdk
  • native XRP deposits using drops as bigint
  • issued-currency deposits using CUR.rIssuer / CUR:rIssuer asset keys and decimal string amounts
  • XRPL Payment memo encoding for the Clearnet destination account and optional reference
  • transaction verification through XRPL validated transaction lookup
  • XRPL unit tests and Docker-backed integration tests for native XRP and issued-currency deposits
  • a browser UI demo for XRPL deposits with both local signer and GemWallet paths

Decisions Incorporated

  • Keep the TypeScript XRPL depositor aligned with the existing Go flow where practical.
  • Support issued currencies in the first XRPL TypeScript pass.
  • Keep the SDK wallet-agnostic through the XrplSigner interface.
  • Keep local-keypair signing scoped to tests and the local browser demo.
  • Keep GemWallet integration scoped to the browser demo rather than the SDK runtime package.
  • Keep exported functionality minimal: depositor, constants, signer/input types, and validation-backed config.
  • Use neutral defaults for fee handling, with an optional maxFeeDrops ceiling for callers that want one.
  • Preserve the shared destination.account / destination.ref deposit shape across chains.

Demo Recordings

Local Signer Demo

ScreenRecording2026-06-26at3 09 36PM-ezgif com-video-to-gif-converter

GemWallet Demo

ScreenRecording2026-06-26at3 09 05PM-ezgif com-video-to-gif-converter

XRPL Demo Setup Notes

The XRPL demo now has two paths:

  • Local signer: fastest smoke test for the standalone devnet. It generates or reuses a browser-local XRPL seed, funds that wallet from the standalone genesis wallet, submits a deposit, calls ledger_accept, and verifies the last transaction.
  • GemWallet: browser-wallet signing path. GemWallet must be pointed at the same chain as the demo through a custom wss:// endpoint.

The local rippled devnet uses network_id: 31337. This intentionally avoids 21337 and 21338, which are Xahau mainnet/testnet IDs. Reusing those IDs made the local chain look like Xahau to wallet tooling and caused signing/submission failures that were hard to distinguish from SDK issues.

For the GemWallet path, the local rippled WebSocket (ws://127.0.0.1:6006) needs a WSS tunnel or TLS proxy because GemWallet custom networks require wss://. The working setup was:

  1. Run local XRPL devnet.
  2. Expose 127.0.0.1:6006 through a WSS endpoint, for example with ngrok.
  3. Add that WSS endpoint as a GemWallet custom network.
  4. Set the demo WebSocket URL to the same WSS endpoint.
  5. Keep Admin HTTP URL as /xrpl-admin so the local demo can call standalone ledger_accept.
  6. Connect GemWallet, fund the selected wallet, submit the deposit, approve signing, then verify.

The full runbook is documented in sdk/ts/examples/xrpl-deposit/README.md.

Issue Found During Demo Validation

The GemWallet path had two different failure modes:

  • If GemWallet stayed on Xahau testnet while the demo submitted to local rippled, the wallet and app were signing/submitting against different networks. The demo now checks GemWallet's selected endpoint and compares server_info.network_id with the demo RPC before opening the signing request.
  • GemWallet 3.8.2 failed to render its signing review for a prepared transaction that already included the local custom NetworkID: 31337. The demo now removes NetworkID only from the transaction object passed to GemWallet after the endpoint/network check. GemWallet autofills NetworkID from its selected custom endpoint before signing; tx lookup confirmed the submitted transaction carried NetworkID: 31337 and validated with tesSUCCESS.

Validation

  • make devnet
  • make integration
  • npm --prefix sdk/ts run typecheck
  • npm --prefix sdk/ts test
  • npm --prefix sdk/ts run build
  • npm --prefix sdk/ts --workspace @yellow-org/xrpl-deposit-demo run build
  • npm --prefix sdk/ts audit --omit=dev --audit-level=moderate
  • git diff --check

Live XRPL demo checks completed locally:

  • local signer deposit submitted and verified as confirmed
  • GemWallet custom WSS deposit submitted and verified as confirmed
  • direct tx lookup for the GemWallet-submitted hash returned validated: true, TransactionResult: tesSUCCESS, NetworkID: 31337, and delivered_amount: 1000000

@ihsraham ihsraham marked this pull request as ready for review June 26, 2026 09:54
@ihsraham ihsraham force-pushed the feat/xrpl-ts-depositor branch from c37fc66 to ed1283a Compare June 26, 2026 10:05

@nksazonov nksazonov left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work overall — the PR delivers exactly what the description promises: XrplVaultDepositor, native and issued-currency paths, a full unit test suite, Docker-backed integration tests, and the browser demo with both local-signer and GemWallet paths. The code is clean and consistent with the existing depositor patterns. There are a few small things to address before landing.

minConfirmations: bigint | number,
): Promise<DepositStatus> {
const normalized = requireTxRef(ref);
normalizeMinConfirmations(minConfirmations);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of normalizeMinConfirmations(minConfirmations) is discarded. Both EvmVaultDepositor (evm/depositor.ts:85) and SolanaVaultDepositor (sol/depositor.ts:128) assign it as const minConf = normalizeMinConfirmations(...) and use it to gate the "confirmed" return. Here the result is thrown away, so verifyDeposit(ref, 0) and verifyDeposit(ref, 100) behave identically — if the tx is in a validated ledger, it returns "confirmed" immediately.

XRPL finality is binary (a transaction in a validated ledger is final; there's no block-depth equivalent), so the parameter genuinely cannot be honoured the way EVM/Solana honour it. Callers who copy the EVM/Solana pattern expecting a minimum threshold to be enforced are silently wrong.

Suggestion: add a code comment on this line explaining that XRPL finality is binary so the parameter is validated for interface consistency but has no effect on the returned status.

private readonly signer: XrplSigner;
private readonly vaultAddress: string;
private readonly maxFeeDrops: bigint | undefined;
private readonly client: Client;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XrplVaultDepositor is the first depositor in this SDK backed by a persistent WebSocket (EVM and Solana use HTTP). The connection is opened lazily by ensureConnected() but there is no disconnect() method — callers have no way to close it.

The demo at main.ts compounds this: it creates a new depositor instance at lines 182 and 233 on every button click, opening a fresh connection each time without cleaning up the prior one. Server-side code that creates depositors per request will accumulate open WebSocket connections.

Suggestion: expose async disconnect(): Promise<void> { if (this.client.isConnected()) await this.client.disconnect(); } and reuse a single depositor instance across actions in the demo rather than constructing one per call.

if (!destination || typeof destination !== "object") {
throw new ClearnetSdkError(
"INVALID_ADDRESS",
"destination.account must be a 20-byte hex address",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "destination.account must be a 20-byte hex address" fires at this line when destination is null or not an object — before destination.account is ever accessed. Lines 90 and 98 use the same message for the actual account validation, which is correct, but the message at line 80 blames a nested field for a missing parent.

Suggestion: use "destination is required and must be an object" at this line.

function requireSubmitDepositOptions(options: unknown): SubmitDepositOptions {
if (options === null || typeof options !== "object") {
throw new ClearnetSdkError(
"RPC_ERROR",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RPC_ERROR is used throughout the codebase for XRPL node failures (network errors, rejected transactions). A null options argument is a caller mistake, not a node event. Callers who catch RPC_ERROR to handle connectivity problems will accidentally swallow this validation error.

Suggestion: add "INVALID_INPUT" to the ClearnetSdkErrorCode union in core/errors.ts and use it here, or at minimum use one of the existing INVALID_* codes.

@dimast-x dimast-x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to merge once Nikita's comments are resolved

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.

3 participants