Skip to content

feat: XRPL platform sdk#989

Merged
emreboga merged 2 commits intomainfrom
add_xrpl_platform
Feb 25, 2026
Merged

feat: XRPL platform sdk#989
emreboga merged 2 commits intomainfrom
add_xrpl_platform

Conversation

@douglasgalico
Copy link
Contributor

@douglasgalico douglasgalico commented Feb 25, 2026

Summary by CodeRabbit

  • New Features

    • Added XRPL platform: signer, chain context, address types, unsigned transaction type, and native balance/decimals support
    • Switched XRPL RPC endpoints to WebSocket (wss)
  • Packaging / Chores

    • Added XRPL package to monorepo workspaces and SDK exports; dual ESM/CJS packaging configured
  • Tests

    • Added unit and integration tests covering XRPL addresses, signer behavior, and RPC interactions

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Adds a full XRPL platform to the monorepo: new platform package, types, address/chain implementations, signer, unsigned transaction type, RPC config update to wss, SDK integration and exports, build/test configs, and unit/integration tests.

Changes

Cohort / File(s) Summary
RPC Configuration Updates
core/base/src/constants/rpc.ts
Changed XRPL RPC endpoint URLs from HTTPS to WebSocket (wss) for mainnet and testnet entries.
Monorepo & SDK References
package.json, sdk/tsconfig.cjs.json, sdk/tsconfig.esm.json
Added platforms/xrpl to repo workspaces and TypeScript build references so XRPL is included in SDK builds.
XRPL Package Manifest & Build
platforms/xrpl/package.json, platforms/xrpl/tsconfig.cjs.json, platforms/xrpl/tsconfig.esm.json, platforms/xrpl/jest.config.ts
New package manifest with dual ESM/CJS exports, scripts, typesVersions, and tsconfig/jest configs for building/testing the XRPL package.
XRPL Core Types & Barrel
platforms/xrpl/src/types.ts, platforms/xrpl/src/index.ts
Introduces platform identifier, chain/address unions and a barrel file re-exporting package modules.
XRPL Address Implementation
platforms/xrpl/src/address.ts
New XrplAddress class, XrplZeroAddress constant, serialization helpers, validation, UniversalAddress conversions, and registration with SDK registry.
XRPL Chain Context
platforms/xrpl/src/chain.ts
Adds XrplChain class extending generic ChainContext for XRPL chains.
XRPL Platform Implementation
platforms/xrpl/src/platform.ts
New XrplPlatform: RPC provisioning (Client), getChain, chain-from-RPC logic, native token helpers, balance/decimals via RPC, and placeholders for blocks/send.
XRPL Signer & Unsigned Tx
platforms/xrpl/src/signer.ts, platforms/xrpl/src/unsignedTransaction.ts
Adds getXrplSigner factory and XrplSigner implementing signing and sign-and-send flows; introduces XrplUnsignedTransaction data wrapper.
SDK Integration & Exports
sdk/package.json, sdk/src/platforms/xrpl.ts, sdk/src/xrpl.ts, sdk/src/addresses.ts
Added XRPL dependency and package exports, lazy-loading platform adapter, platform definition mapping, and imported XRPL address registration into SDK addresses.
XRPL Tests
platforms/xrpl/__tests__/unit/*.test.ts, platforms/xrpl/__tests__/integration/*.test.ts
New unit and integration tests covering address handling, signer behavior, platform RPC interactions, and testnet signing/submission.
Miscellaneous
sdk/package.json
Added @wormhole-foundation/sdk-xrpl dependency and updated SDK package exports/typesVersions to include XRPL typings.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Signer as XrplSigner
    participant Client as XRPL_Client
    participant Ledger as XRP_Ledger

    User->>Signer: sign(transactions[])
    activate Signer
    Signer->>Client: connect() if not connected
    activate Client
    Client->>Ledger: open websocket / establish session
    Ledger-->>Client: connection established
    Client-->>Signer: connected
    deactivate Client

    loop per transaction
      Signer->>Client: autofill(tx)
      activate Client
      Client->>Ledger: request state (fees, sequence, etc.)
      Ledger-->>Client: state
      Client-->>Signer: autofilled tx
      deactivate Client

      Signer->>Signer: sign with Wallet.fromSeed -> signed blob
    end

    Signer->>User: return SignedTx[] (blobs)
    deactivate Signer
Loading
sequenceDiagram
    participant User
    participant Signer as XrplSigner
    participant Client as XRPL_Client
    participant Ledger as XRP_Ledger

    User->>Signer: signAndSend(transactions[])
    activate Signer
    Signer->>Client: connect() if needed
    activate Client
    Client->>Ledger: open websocket
    Ledger-->>Client: connected
    Client-->>Signer: connected
    deactivate Client

    loop per transaction
      Signer->>Client: autofill(tx)
      activate Client
      Client->>Ledger: fetch state
      Ledger-->>Client: state
      Client-->>Signer: autofilled tx
      deactivate Client

      Signer->>Signer: sign with Wallet
      Signer->>Client: submit(signedTx)
      activate Client
      Client->>Ledger: submit tx
      Ledger-->>Client: tx hash & provisional status
      Client-->>Signer: tx hash
      deactivate Client

      Signer->>Client: waitForTransaction(txHash)
      activate Client
      Client->>Ledger: poll/subscribe for finality
      Ledger-->>Client: confirmed
      Client-->>Signer: confirmed
      deactivate Client
    end

    Signer->>User: return TxHash[] (64-char hex)
    deactivate Signer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • kev1n-peters

Poem

🐰
I hopped through code with eager paws,
Wss lines stitched without a pause.
New XRPL fields, addresses bright,
Signers singing through the night.
A carrot-tokened cheer — hip hop hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: XRPL platform sdk' accurately describes the main objective of this changeset—adding a complete XRPL platform integration to the Wormhole SDK.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add_xrpl_platform

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (8)
platforms/xrpl/src/types.ts (1)

15-15: Consider narrowing AnyXrplAddress to UniversalOrXrpl | string if Uint8Array is not a first-class XRPL address representation.

Looking at the analogous type in other platforms (e.g., EVM, Solana), Uint8Array is typically included only when the platform natively uses raw byte addresses (e.g., for 32-byte public keys). XRPL uses base58-check encoded account addresses (r...), so accepting Uint8Array here could introduce silent misuse at call sites without validation in XrplAddress.

Verify whether platforms/xrpl/src/address.ts actually handles Uint8Array input before keeping this in the union.

#!/bin/bash
# Description: Check if XrplAddress constructor handles Uint8Array input.
rg -n "Uint8Array" platforms/xrpl/src/address.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/types.ts` at line 15, The type AnyXrplAddress currently
includes Uint8Array but XRPL addresses are base58-check strings; inspect
XrplAddress (in platforms/xrpl/src/address.ts) for explicit
handling/conversion/validation of Uint8Array inputs—if XrplAddress does not
accept or convert Uint8Array, remove Uint8Array from the AnyXrplAddress union in
types.ts to prevent silent misuse; if it does accept them, ensure the
constructor/method (XrplAddress) normalizes/validates the Uint8Array to a
canonical base58 address and add tests demonstrating that path.
platforms/xrpl/package.json (1)

54-60: typesVersions only maps to the index entry — the ./address sub-export has no entry.

Consumers on older TypeScript versions (< 4.7) that rely on typesVersions rather than exports conditions won't resolve types for @wormhole-foundation/sdk-xrpl/address. Add an entry for the address sub-path if you intend to support those consumers.

Proposed fix
   "typesVersions": {
     "*": {
-      "*": [
+      ".": [
         "dist/cjs/index.d.ts"
+      ],
+      "address": [
+        "dist/cjs/address.d.ts"
       ]
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/package.json` around lines 54 - 60, The typesVersions mapping
only points to "dist/cjs/index.d.ts" so the address sub-export
(`@wormhole-foundation/sdk-xrpl/address`) has no type entry; update the
package.json typesVersions section to add a "./address" key mapping to the built
declaration for that subpath (e.g., point "./address" to "dist/cjs/address.d.ts"
or the correct emitted .d.ts file), ensuring the typesVersions object covers
both the root index and the address subpath so older TypeScript consumers
resolve types for the address export.
platforms/xrpl/src/signer.ts (3)

26-28: _network is accepted but discarded.

The constructor parameter _network: Network lacks the private modifier, so it is not stored. If SignAndSendSigner (or any future code) needs the network, this will silently fail. Either store it or remove it if truly unused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/signer.ts` around lines 26 - 28, The constructor currently
accepts _network: Network but doesn't store it (missing the private modifier),
so either remove the parameter if unused or persist it for later use; update the
constructor signature in the Signer class to include a private (or readonly)
modifier for _network (e.g., private _network: Network) so instances can access
the network, and adjust any references in methods like SignAndSendSigner to use
this._network; if you choose to remove it, also delete any references and tests
expecting the parameter.

13-18: Parameter privateKey is actually an XRPL seed — rename for clarity.

Wallet.fromSeed() expects a seed string, not a private key. The current naming is misleading and could lead consumers to pass an actual private key (hex) instead of an XRPL seed (e.g., sEd...).

Proposed rename
 export async function getXrplSigner(
   rpc: Client,
-  privateKey: string,
+  seed: string,
 ): Promise<SignAndSendSigner<Network, XrplChains>> {
   const [network, chain] = await XrplPlatform.chainFromRpc(rpc);
-  return new XrplSigner(chain, network, rpc, privateKey);
+  return new XrplSigner(chain, network, rpc, seed);
 }
   constructor(
     private _chain: C,
     _network: Network,
     private _provider: Client,
-    privateKey: string,
+    seed: string,
   ) {
-    this._wallet = Wallet.fromSeed(privateKey);
+    this._wallet = Wallet.fromSeed(seed);
   }

Also applies to: 26-33

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/signer.ts` around lines 13 - 18, The parameter named
privateKey is actually an XRPL seed; rename it to seed in the getXrplSigner
function signature and all related usages to avoid confusion (e.g., change
privateKey -> seed in getXrplSigner and the other function at lines 26-33), and
update any calls into XrplSigner or Wallet.fromSeed to pass the renamed seed
variable; ensure parameter types and JSDoc/comments reflect "seed (XRPL seed
string)" and that XrplSigner construction and Wallet.fromSeed invocations use
the new seed identifier.

35-51: Misleading variable name connected — it actually means "we opened the connection".

const connected = !this._provider.isConnected() is true when the provider was not connected, meaning "we need to connect (and later disconnect)." This inverted semantics is error-prone and appears in both signAndSend and sign.

Proposed rename
-    const connected = !this._provider.isConnected();
-    if (connected) await this._provider.connect();
+    const needsConnect = !this._provider.isConnected();
+    if (needsConnect) await this._provider.connect();
     ...
-      if (connected) await this._provider.disconnect();
+      if (needsConnect) await this._provider.disconnect();

Apply the same rename in sign() (Lines 55-68) and in platform.ts Lines 97-107 and 138-150 where the identical pattern is used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/signer.ts` around lines 35 - 51, The local boolean
variable currently named connected has inverted semantics (it is true when the
provider was NOT connected), so rename it to a clear name like needToConnect or
shouldConnect in signAndSend and sign to reflect "we need to open the connection
(and later close it)"; update both the variable declaration and its usage in the
connect()/disconnect() logic and ensure the finally block uses the new name, and
apply the same rename and adjustments to the identical pattern in the Platform
class methods in platform.ts so all checks and awaits remain logically
consistent.
platforms/xrpl/src/platform.ts (2)

89-111: getBalance for non-native tokens throws outside the connection management block — verify this is intentional.

Line 110 throws after the if (isNative(token)) block, meaning the rpc connection is never opened for non-native tokens. This is correct (no leak), but the control flow is subtle. A guard clause at the top would make intent clearer:

Suggested restructure
   static async getBalance<C extends XrplChains>(
     _network: Network,
     _chain: C,
     rpc: Client,
     walletAddr: string,
     token: TokenAddress<C>,
   ): Promise<bigint | null> {
+    if (!isNative(token)) {
+      throw new Error("Token balance lookup not yet implemented for XRPL");
+    }
-    if (isNative(token)) {
-      const connected = !rpc.isConnected();
-      if (connected) await rpc.connect();
-      try {
-        const response = await rpc.request({
-          command: "account_info",
-          account: walletAddr,
-          ledger_index: "validated",
-        });
-        return BigInt(response.result.account_data.Balance);
-      } finally {
-        if (connected) await rpc.disconnect();
-      }
-    }
-    throw new Error("Token balance lookup not yet implemented for XRPL");
+    const needsConnect = !rpc.isConnected();
+    if (needsConnect) await rpc.connect();
+    try {
+      const response = await rpc.request({
+        command: "account_info",
+        account: walletAddr,
+        ledger_index: "validated",
+      });
+      return BigInt(response.result.account_data.Balance);
+    } finally {
+      if (needsConnect) await rpc.disconnect();
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/platform.ts` around lines 89 - 111, The control flow in
getBalance is subtle because the non-native token error is thrown after the
native-only branch, which can confuse reviewers; refactor getBalance by adding
an early guard clause using isNative(token) (or its negation) so non-native
tokens immediately throw before any rpc.connect/disconnect logic, and keep the
existing connection management (the connected check, rpc.connect, try/finally
with rpc.disconnect) inside the native-token branch; reference getBalance,
isNative, rpc.connect, rpc.disconnect, and the thrown Error message to locate
and update the logic.

38-41: getRpc creates a new disconnected Client on every call.

Each invocation returns a fresh Client instance that is not connected. This means every consumer must manage connect()/disconnect() lifecycles, and there's no connection reuse. This is workable but worth documenting — callers that forget to connect will get confusing errors.

platforms/xrpl/__tests__/unit/address.test.ts (1)

3-57: Good baseline coverage. Consider adding a few edge-case tests.

The tests cover the main API surface well. A couple of gaps worth adding:

  1. equals() with a UniversalAddress — exercises the else branch at address.ts Line 67.
  2. equals() returning false for two different addresses — confirms inequality logic.
  3. XrplAddress.instanceof() — the static type guard used internally.

These would improve branch coverage without much effort.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/__tests__/unit/address.test.ts` around lines 3 - 57, Add three
unit tests to exercise the equals and instanceof branches: (1) a test that calls
XrplAddress.equals with a UniversalAddress instance (create a UniversalAddress
from a known XrplAddress and assert XrplAddress.equals(universal) returns true)
to hit the else-branch in XrplAddress.equals; (2) a test that constructs two
different XrplAddress values (two distinct valid addresses) and asserts equals
returns false to verify inequality logic; and (3) a test for the static type
guard XrplAddress.instanceof by passing an object shaped like an address and a
non-address and asserting true/false accordingly so the instanceof guard is
covered. Ensure you use the existing helpers: XrplAddress, UniversalAddress,
XrplZeroAddress (or another valid addresses), and the class methods equals(),
toUniversalAddress(), and instanceof() in your assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platforms/xrpl/__tests__/integration/platform.test.ts`:
- Around line 9-11: Replace the hardcoded always-funded testnet account by
funding a fresh wallet in the test setup: in the test file's beforeAll, call the
XRPL client.fundWallet() (as used in signer.test.ts) to create and store a
funded wallet instead of using TESTNET_ACCOUNT; update tests to reference the
funded wallet variable (remove reliance on the TESTNET_ACCOUNT constant) so the
suite is self-contained and survives ledger resets.

In `@platforms/xrpl/__tests__/integration/signer.test.ts`:
- Around line 55-75: The test for XrplSigner.signAndSend is using a hardcoded
Destination that may not exist on Testnet, causing tecNO_DST; modify the test
setup to create and fund a second wallet in the beforeAll (e.g.,
secondFundedWallet) and use secondFundedWallet.classicAddress as the Destination
in the XrplUnsignedTransaction used by the test, or alternatively send an amount
>= base reserve from the fundedWallet to that destination to ensure the account
is activated before calling signAndSend; update any references in the test to
use the new funded wallet variable and keep XrplSigner and signAndSend behavior
unchanged.

In `@platforms/xrpl/package.json`:
- Around line 49-52: The xrpl dependency is using a caret range ("^4.2.0") that
can pull in compromised 4.2.x releases; update the dependency in package.json to
either pin to "4.2.0" exactly or bump to a safe range (e.g., "^4.2.5" or later)
and then run CI/build to verify no breaking changes; also audit usages of xrpl
APIs in this package—specifically references to Client, Wallet.fromSeed,
isValidClassicAddress, decodeAccountID, and SubmittableTransaction—to ensure
those symbols exist and behave the same in the chosen xrpl version, updating
imports or call sites if needed.

In `@platforms/xrpl/src/address.ts`:
- Around line 59-61: The static instanceof method on XrplAddress dereferences
address (address.constructor.platform) and will throw for null/undefined; update
XrplAddress.instanceof to first guard that address is a non-null object and that
address.constructor exists before comparing its platform to XrplAddress.platform
(e.g., check address !== null && typeof address === 'object' &&
address.constructor && address.constructor.platform === XrplAddress.platform) so
that null/undefined or non-object inputs safely return false rather than
throwing.
- Around line 15-29: The constructor currently accepts AnyXrplAddress but only
handles XrplAddress instances and strings; update the constructor to also accept
Uint8Array by converting the bytes back to a classic XRPL address (use your
project’s account ID/address encode helper or xrpl library helper) and then
validate with XrplAddress.isValidAddress before assigning to this.address; keep
throwing on other types. Also harden XrplAddress.instanceof by guarding access
to address.constructor and address.constructor.platform (e.g., check address !=
null and address.constructor != null) before comparing platform to avoid runtime
errors when unexpected values are passed.

In `@platforms/xrpl/src/platform.ts`:
- Around line 137-152: chainFromRpc currently treats any non-zero network_id as
"Testnet", which misclassifies Devnet (network_id = 2); update chainFromRpc to
explicitly map known network_id values to networks: treat network_id === 0 or
network_id === undefined as "Mainnet", network_id === 2 as "Devnet", and return
"Testnet" for other known testnet ids (or default to "Testnet") — implement this
mapping in the function (use networkId variable and the chainFromRpc function)
via a switch/if-else so Devnet is returned correctly.

---

Nitpick comments:
In `@platforms/xrpl/__tests__/unit/address.test.ts`:
- Around line 3-57: Add three unit tests to exercise the equals and instanceof
branches: (1) a test that calls XrplAddress.equals with a UniversalAddress
instance (create a UniversalAddress from a known XrplAddress and assert
XrplAddress.equals(universal) returns true) to hit the else-branch in
XrplAddress.equals; (2) a test that constructs two different XrplAddress values
(two distinct valid addresses) and asserts equals returns false to verify
inequality logic; and (3) a test for the static type guard
XrplAddress.instanceof by passing an object shaped like an address and a
non-address and asserting true/false accordingly so the instanceof guard is
covered. Ensure you use the existing helpers: XrplAddress, UniversalAddress,
XrplZeroAddress (or another valid addresses), and the class methods equals(),
toUniversalAddress(), and instanceof() in your assertions.

In `@platforms/xrpl/package.json`:
- Around line 54-60: The typesVersions mapping only points to
"dist/cjs/index.d.ts" so the address sub-export
(`@wormhole-foundation/sdk-xrpl/address`) has no type entry; update the
package.json typesVersions section to add a "./address" key mapping to the built
declaration for that subpath (e.g., point "./address" to "dist/cjs/address.d.ts"
or the correct emitted .d.ts file), ensuring the typesVersions object covers
both the root index and the address subpath so older TypeScript consumers
resolve types for the address export.

In `@platforms/xrpl/src/platform.ts`:
- Around line 89-111: The control flow in getBalance is subtle because the
non-native token error is thrown after the native-only branch, which can confuse
reviewers; refactor getBalance by adding an early guard clause using
isNative(token) (or its negation) so non-native tokens immediately throw before
any rpc.connect/disconnect logic, and keep the existing connection management
(the connected check, rpc.connect, try/finally with rpc.disconnect) inside the
native-token branch; reference getBalance, isNative, rpc.connect,
rpc.disconnect, and the thrown Error message to locate and update the logic.

In `@platforms/xrpl/src/signer.ts`:
- Around line 26-28: The constructor currently accepts _network: Network but
doesn't store it (missing the private modifier), so either remove the parameter
if unused or persist it for later use; update the constructor signature in the
Signer class to include a private (or readonly) modifier for _network (e.g.,
private _network: Network) so instances can access the network, and adjust any
references in methods like SignAndSendSigner to use this._network; if you choose
to remove it, also delete any references and tests expecting the parameter.
- Around line 13-18: The parameter named privateKey is actually an XRPL seed;
rename it to seed in the getXrplSigner function signature and all related usages
to avoid confusion (e.g., change privateKey -> seed in getXrplSigner and the
other function at lines 26-33), and update any calls into XrplSigner or
Wallet.fromSeed to pass the renamed seed variable; ensure parameter types and
JSDoc/comments reflect "seed (XRPL seed string)" and that XrplSigner
construction and Wallet.fromSeed invocations use the new seed identifier.
- Around line 35-51: The local boolean variable currently named connected has
inverted semantics (it is true when the provider was NOT connected), so rename
it to a clear name like needToConnect or shouldConnect in signAndSend and sign
to reflect "we need to open the connection (and later close it)"; update both
the variable declaration and its usage in the connect()/disconnect() logic and
ensure the finally block uses the new name, and apply the same rename and
adjustments to the identical pattern in the Platform class methods in
platform.ts so all checks and awaits remain logically consistent.

In `@platforms/xrpl/src/types.ts`:
- Line 15: The type AnyXrplAddress currently includes Uint8Array but XRPL
addresses are base58-check strings; inspect XrplAddress (in
platforms/xrpl/src/address.ts) for explicit handling/conversion/validation of
Uint8Array inputs—if XrplAddress does not accept or convert Uint8Array, remove
Uint8Array from the AnyXrplAddress union in types.ts to prevent silent misuse;
if it does accept them, ensure the constructor/method (XrplAddress)
normalizes/validates the Uint8Array to a canonical base58 address and add tests
demonstrating that path.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd46b12 and 4786942.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • core/base/src/constants/rpc.ts
  • package.json
  • platforms/xrpl/__tests__/integration/platform.test.ts
  • platforms/xrpl/__tests__/integration/signer.test.ts
  • platforms/xrpl/__tests__/unit/address.test.ts
  • platforms/xrpl/__tests__/unit/signer.test.ts
  • platforms/xrpl/jest.config.ts
  • platforms/xrpl/package.json
  • platforms/xrpl/src/address.ts
  • platforms/xrpl/src/chain.ts
  • platforms/xrpl/src/index.ts
  • platforms/xrpl/src/platform.ts
  • platforms/xrpl/src/signer.ts
  • platforms/xrpl/src/types.ts
  • platforms/xrpl/src/unsignedTransaction.ts
  • platforms/xrpl/tsconfig.cjs.json
  • platforms/xrpl/tsconfig.esm.json
  • sdk/package.json
  • sdk/src/addresses.ts
  • sdk/src/platforms/xrpl.ts
  • sdk/src/xrpl.ts
  • sdk/tsconfig.cjs.json
  • sdk/tsconfig.esm.json

Comment on lines +9 to +11
// Well-known genesis / faucet-funded testnet account
// (always exists, always has a balance)
const TESTNET_ACCOUNT = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Testnet account may not survive ledger resets.

rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe is described as "always exists, always has a balance," but the XRPL testnet is periodically reset, which deletes all accounts. Consider funding a fresh wallet via client.fundWallet() in beforeAll (as done in signer.test.ts) to make the test self-contained and resilient to resets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/__tests__/integration/platform.test.ts` around lines 9 - 11,
Replace the hardcoded always-funded testnet account by funding a fresh wallet in
the test setup: in the test file's beforeAll, call the XRPL client.fundWallet()
(as used in signer.test.ts) to create and store a funded wallet instead of using
TESTNET_ACCOUNT; update tests to reference the funded wallet variable (remove
reliance on the TESTNET_ACCOUNT constant) so the suite is self-contained and
survives ledger resets.

Comment on lines +55 to +75
it("signAndSend() submits a payment and returns a tx hash", async () => {
const signer = new XrplSigner("Xrpl", "Testnet", client, fundedWallet.seed!);

const unsignedTx = new XrplUnsignedTransaction(
{
TransactionType: "Payment",
Account: fundedWallet.classicAddress,
Destination: "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
Amount: "1000000", // 1 XRP in drops
},
"Testnet",
"Xrpl",
"Test payment sign-and-send",
);

const hashes = await signer.signAndSend([unsignedTx]);
expect(hashes).toHaveLength(1);
expect(typeof hashes[0]).toBe("string");
// XRPL tx hashes are 64 hex chars
expect(hashes[0]).toMatch(/^[A-F0-9]{64}$/);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

signAndSend test depends on the destination account existing on testnet.

XRPL requires the destination of a Payment to be an existing (activated) account unless the amount meets the reserve requirement. If rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe is deleted during a testnet reset, this test will fail with a tecNO_DST error. Consider either:

  • Funding a second wallet in beforeAll and using it as the destination, or
  • Sending enough XRP to activate the account (≥ base reserve) to handle the case where the account doesn't exist.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/__tests__/integration/signer.test.ts` around lines 55 - 75,
The test for XrplSigner.signAndSend is using a hardcoded Destination that may
not exist on Testnet, causing tecNO_DST; modify the test setup to create and
fund a second wallet in the beforeAll (e.g., secondFundedWallet) and use
secondFundedWallet.classicAddress as the Destination in the
XrplUnsignedTransaction used by the test, or alternatively send an amount >=
base reserve from the fundedWallet to that destination to ensure the account is
activated before calling signAndSend; update any references in the test to use
the new funded wallet variable and keep XrplSigner and signAndSend behavior
unchanged.

Comment on lines +49 to +52
"dependencies": {
"xrpl": "^4.2.0",
"@wormhole-foundation/sdk-connect": "1.20.0"
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what version of sdk-connect other platform packages use
rg '"@wormhole-foundation/sdk-connect"' --type=json -g 'platforms/*/package.json' -A 0

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 710


🌐 Web query:

xrpl npm package version 4.2.0 availability and compatibility

💡 Result:

  • Availability: xrpl v4.2.0 is a published, installable npm release (it has its own packaged package.json with "version": "4.2.0"). [1]
  • Compatibility (runtime): xrpl@4.2.0 declares Node.js >=18.0.0 via engines, and it’s intended for Node.js and the browser (has a browser field and ships UMD bundles via unpkg/jsdelivr). [1]
  • What changed in 4.2.0: The xrpl@4.2.0 GitHub release notes list additions like DynamicNFT support, simulate RPC (XLS-69d), PermissionedDomain (XLS-80d), Deep Freeze (XLS-77d), and AMMClawback support. [2]
  • Security / version selection: xrpl@4.2.0 is not in the known-compromised range (4.2.1–4.2.4) from the April 2025 supply-chain incident; patched safe versions were released afterward (e.g., 4.2.5+). If you can, prefer upgrading beyond 4.2.0 (at least 4.2.5). [3][4]

Sources: [1] [2] [3] [4]


Pin xrpl to safe version and verify API compatibility.

The @wormhole-foundation/sdk-connect version (1.20.0) is correctly aligned across all platforms in the monorepo, and xrpl@4.2.0 is a published, compatible release. However, the caret range ^4.2.0 allows installation of versions 4.2.1–4.2.4, which were compromised in a documented supply-chain incident in April 2025. Either pin to exactly "4.2.0" or upgrade to "^4.2.5" or later. Additionally, verify that the APIs used in this package (Client, Wallet.fromSeed, isValidClassicAddress, decodeAccountID, SubmittableTransaction) are available and unchanged in the chosen version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/package.json` around lines 49 - 52, The xrpl dependency is
using a caret range ("^4.2.0") that can pull in compromised 4.2.x releases;
update the dependency in package.json to either pin to "4.2.0" exactly or bump
to a safe range (e.g., "^4.2.5" or later) and then run CI/build to verify no
breaking changes; also audit usages of xrpl APIs in this package—specifically
references to Client, Wallet.fromSeed, isValidClassicAddress, decodeAccountID,
and SubmittableTransaction—to ensure those symbols exist and behave the same in
the chosen xrpl version, updating imports or call sites if needed.

Comment on lines +15 to +29
constructor(address: AnyXrplAddress) {
if (XrplAddress.instanceof(address)) {
const a = address as unknown as XrplAddress;
this.address = a.address;
return;
}

if (typeof address === "string") {
if (!XrplAddress.isValidAddress(address)) {
throw new Error(`Invalid XRPL address: ${address}`);
}
this.address = address;
} else {
throw new Error(`Invalid XRPL address: ${address}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Uint8Array is ever passed to XrplAddress constructor
rg -n "new XrplAddress" --type=ts -C3

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 4053


🏁 Script executed:

#!/bin/bash
# Check the AnyXrplAddress type definition
fd "types.ts" --type=f -path "*/xrpl/*" | head -5

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 247


🏁 Script executed:

#!/bin/bash
# Examine the address.ts file to understand the full constructor and instanceof implementation
cat -n platforms/xrpl/src/address.ts | head -80

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 2847


🏁 Script executed:

#!/bin/bash
# Search for AnyXrplAddress type definition and usage
rg -n "AnyXrplAddress" --type=ts -B2 -A2

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 1038


🏁 Script executed:

#!/bin/bash
# Check UniversalOrXrpl type to understand what XrplAddress instances include
rg -n "UniversalOrXrpl" --type-add "ts:*.ts" -t ts -B5 -A5

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 568


🏁 Script executed:

#!/bin/bash
# Search for any Uint8Array usage specifically with XrplAddress or address construction
rg -n "Uint8Array" platforms/xrpl/ --type-add "ts:*.ts" -t ts -C3

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 2206


🏁 Script executed:

#!/bin/bash
# Check if instanceof is called with null/undefined anywhere or if there are guards
rg -n "instanceof" platforms/xrpl/src/ --type-add "ts:*.ts" -t ts -B2 -A2

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 1038


🏁 Script executed:

#!/bin/bash
# Look at the imports and types to understand the full picture
cat -n platforms/xrpl/src/types.ts

Repository: wormhole-foundation/wormhole-sdk-ts

Length of output: 639


Constructor does not handle Uint8Array despite AnyXrplAddress including it.

AnyXrplAddress is typed as UniversalOrXrpl | string | Uint8Array (types.ts line 15), but the constructor only handles XrplAddress instances and string. Passing a Uint8Array falls into the else branch and throws. Either narrow AnyXrplAddress to exclude Uint8Array, or add handling for it (e.g., encode the account ID back to a classic address).

Additionally, the instanceof method (line 59) accesses address.constructor.platform without guarding against null or undefined. While the type signature doesn't include these, add a safety check to prevent runtime errors if the method is called with unexpected values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/address.ts` around lines 15 - 29, The constructor
currently accepts AnyXrplAddress but only handles XrplAddress instances and
strings; update the constructor to also accept Uint8Array by converting the
bytes back to a classic XRPL address (use your project’s account ID/address
encode helper or xrpl library helper) and then validate with
XrplAddress.isValidAddress before assigning to this.address; keep throwing on
other types. Also harden XrplAddress.instanceof by guarding access to
address.constructor and address.constructor.platform (e.g., check address !=
null and address.constructor != null) before comparing platform to avoid runtime
errors when unexpected values are passed.

Comment on lines +59 to +61
static instanceof(address: any): address is XrplAddress {
return address.constructor.platform === XrplAddress.platform;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

instanceof will throw on null/undefined input.

address.constructor.platform dereferences address without a null guard. If null or undefined is passed, this throws an unintended TypeError instead of returning false.

Proposed fix
  static instanceof(address: any): address is XrplAddress {
+   if (address == null || typeof address !== "object") return false;
    return address.constructor.platform === XrplAddress.platform;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/address.ts` around lines 59 - 61, The static instanceof
method on XrplAddress dereferences address (address.constructor.platform) and
will throw for null/undefined; update XrplAddress.instanceof to first guard that
address is a non-null object and that address.constructor exists before
comparing its platform to XrplAddress.platform (e.g., check address !== null &&
typeof address === 'object' && address.constructor &&
address.constructor.platform === XrplAddress.platform) so that null/undefined or
non-object inputs safely return false rather than throwing.

Comment on lines 137 to 152
static async chainFromRpc(rpc: Client): Promise<[Network, XrplChains]> {
const connected = !rpc.isConnected();
if (connected) await rpc.connect();
try {
const response = await rpc.request({
command: "server_info",
});
const networkId = response.result.info.network_id;
if (networkId === 0 || networkId === undefined) {
return ["Mainnet", "Xrpl"];
}
return ["Testnet", "Xrpl"];
} finally {
if (connected) await rpc.disconnect();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

chainFromRpc network detection may misclassify Devnet.

network_id === 0 || undefined → Mainnet, everything else → Testnet. XRPL Devnet (typically network_id = 2) would be returned as "Testnet". If Devnet support is planned, this needs a more robust mapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/platform.ts` around lines 137 - 152, chainFromRpc
currently treats any non-zero network_id as "Testnet", which misclassifies
Devnet (network_id = 2); update chainFromRpc to explicitly map known network_id
values to networks: treat network_id === 0 or network_id === undefined as
"Mainnet", network_id === 2 as "Devnet", and return "Testnet" for other known
testnet ids (or default to "Testnet") — implement this mapping in the function
(use networkId variable and the chainFromRpc function) via a switch/if-else so
Devnet is returned correctly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platforms/xrpl/src/signer.ts`:
- Around line 35-67: The signAndSend and sign methods currently unconditionally
disconnect the provider in their finally blocks which can tear down
caller-managed/shared connections; change both methods to track whether this
method actually opened the connection (e.g., a local flag set when calling
this._provider.connect()) and only call this._provider.disconnect() in the
finally block if that flag is true and the provider is still connected, leaving
externally-managed connections intact; update both signAndSend and sign to use
this "openedHere" flag around the existing connect() and disconnect() calls.
- Around line 13-33: Rename the parameter named privateKey to seed in both
getXrplSigner and the XrplSigner constructor to reflect that Wallet.fromSeed()
expects an XRPL Base58Check seed string; update parameter names and any internal
references (_wallet initialization using Wallet.fromSeed(seed)) and adjust
exported function signature and class constructor signature accordingly
(getXrplSigner, XrplSigner, and the Wallet.fromSeed call) so the API clearly
communicates a seed is required rather than a raw hex private key.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4786942 and a3d41cf.

📒 Files selected for processing (2)
  • platforms/xrpl/src/platform.ts
  • platforms/xrpl/src/signer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • platforms/xrpl/src/platform.ts

Comment on lines +13 to +33
export async function getXrplSigner(
rpc: Client,
privateKey: string,
): Promise<SignAndSendSigner<Network, XrplChains>> {
const [network, chain] = await XrplPlatform.chainFromRpc(rpc);
return new XrplSigner(chain, network, rpc, privateKey);
}

export class XrplSigner<N extends Network, C extends XrplChains>
implements SignAndSendSigner<N, C>
{
private readonly _wallet: Wallet;

constructor(
private _chain: C,
_network: Network,
private _provider: Client,
privateKey: string,
) {
this._wallet = Wallet.fromSeed(privateKey);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

XRPL npm package Wallet.fromSeed() method what format accepts seed private key

💡 Result:

Wallet.fromSeed() (aka Wallet.fromSecret) in the xrpl npm package expects an XRPL “seed” / “secret” string in XRPL Base58Check format (not a raw private key). The docs describe it as a “base58 encoded seed”, with examples like ssZkdwURFMBXenJPbrpE14b6noJSu (secp256k1 seeds also commonly start with s...). [1][2]

So:

  • Accepted input: XRPL Base58Check seed value (“seed/secret”), which per XRPL encoding conventions starts with s. [2]
  • Not accepted: a hex private key (e.g., 00... 33-byte hex) or other private-key formats. XRPL “seed” and “private key” are different concepts; software usually derives the private key from the seed. [3]

If you only have a private key, you generally need to construct a wallet from a keypair (publicKey + privateKey) rather than fromSeed, or convert from other material (mnemonic/entropy) into a seed using the appropriate utilities (e.g., encodeSeed/decodeSeed live in ripple-address-codec). [1][4]

Sources:
[1] xrpl.js Wallet docs (Wallet.fromSeed, “base58 encoded seed”)
[2] XRPL base58 encodings (seed values start with s, Base58Check)
[3] XRPL cryptographic keys (seed vs private key)
[4] ripple-address-codec docs (encodeSeed / decodeSeed)


Rename privateKey to seed to match Wallet creation semantics.

At lines 15 and 30, the parameter is named privateKey, but line 32 uses Wallet.fromSeed(), which expects an XRPL Base58Check seed string (e.g., ssZkdwURFMBXenJPbrpE14b6noJSu), not a raw hex private key. This naming mismatch creates an incorrect API contract and invites misuse by developers who may pass a raw private key instead of a seed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/signer.ts` around lines 13 - 33, Rename the parameter
named privateKey to seed in both getXrplSigner and the XrplSigner constructor to
reflect that Wallet.fromSeed() expects an XRPL Base58Check seed string; update
parameter names and any internal references (_wallet initialization using
Wallet.fromSeed(seed)) and adjust exported function signature and class
constructor signature accordingly (getXrplSigner, XrplSigner, and the
Wallet.fromSeed call) so the API clearly communicates a seed is required rather
than a raw hex private key.

Comment on lines +35 to +67
async signAndSend(txs: UnsignedTransaction<N, C>[]): Promise<TxHash[]> {
if (!this._provider.isConnected()) await this._provider.connect();

try {
const txHashes: TxHash[] = [];
for (const tx of txs) {
const xrplTx = tx as XrplUnsignedTransaction<N, C>;
const prepared = await this._provider.autofill(xrplTx.transaction);
const signed = this._wallet.sign(prepared);
const result = await this._provider.submitAndWait(signed.tx_blob);
txHashes.push(result.result.hash);
}
return txHashes;
} finally {
if (this._provider.isConnected()) await this._provider.disconnect();
}
}

async sign(txs: UnsignedTransaction<N, C>[]): Promise<SignedTx[]> {
if (!this._provider.isConnected()) await this._provider.connect();

try {
const signedTxs: SignedTx[] = [];
for (const tx of txs) {
const xrplTx = tx as XrplUnsignedTransaction<N, C>;
const prepared = await this._provider.autofill(xrplTx.transaction);
const signed = this._wallet.sign(prepared);
signedTxs.push(signed.tx_blob);
}
return signedTxs;
} finally {
if (this._provider.isConnected()) await this._provider.disconnect();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve caller-owned RPC connection state instead of always disconnecting.

At Line 36/54 you may connect conditionally, but at Line 49/66 you always disconnect if connected. This tears down caller-managed/shared connections and can break concurrent usage.

🔧 Proposed fix
   async signAndSend(txs: UnsignedTransaction<N, C>[]): Promise<TxHash[]> {
-    if (!this._provider.isConnected()) await this._provider.connect();
+    const shouldDisconnect = !this._provider.isConnected();
+    if (shouldDisconnect) await this._provider.connect();

     try {
       const txHashes: TxHash[] = [];
       for (const tx of txs) {
         const xrplTx = tx as XrplUnsignedTransaction<N, C>;
         const prepared = await this._provider.autofill(xrplTx.transaction);
         const signed = this._wallet.sign(prepared);
         const result = await this._provider.submitAndWait(signed.tx_blob);
         txHashes.push(result.result.hash);
       }
       return txHashes;
     } finally {
-      if (this._provider.isConnected()) await this._provider.disconnect();
+      if (shouldDisconnect && this._provider.isConnected()) await this._provider.disconnect();
     }
   }

   async sign(txs: UnsignedTransaction<N, C>[]): Promise<SignedTx[]> {
-    if (!this._provider.isConnected()) await this._provider.connect();
+    const shouldDisconnect = !this._provider.isConnected();
+    if (shouldDisconnect) await this._provider.connect();

     try {
       const signedTxs: SignedTx[] = [];
       for (const tx of txs) {
         const xrplTx = tx as XrplUnsignedTransaction<N, C>;
         const prepared = await this._provider.autofill(xrplTx.transaction);
         const signed = this._wallet.sign(prepared);
         signedTxs.push(signed.tx_blob);
       }
       return signedTxs;
     } finally {
-      if (this._provider.isConnected()) await this._provider.disconnect();
+      if (shouldDisconnect && this._provider.isConnected()) await this._provider.disconnect();
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/xrpl/src/signer.ts` around lines 35 - 67, The signAndSend and sign
methods currently unconditionally disconnect the provider in their finally
blocks which can tear down caller-managed/shared connections; change both
methods to track whether this method actually opened the connection (e.g., a
local flag set when calling this._provider.connect()) and only call
this._provider.disconnect() in the finally block if that flag is true and the
provider is still connected, leaving externally-managed connections intact;
update both signAndSend and sign to use this "openedHere" flag around the
existing connect() and disconnect() calls.

@emreboga emreboga merged commit db66694 into main Feb 25, 2026
2 checks passed
@emreboga emreboga deleted the add_xrpl_platform branch February 25, 2026 18:56
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