Skip to content

(feat) Support Dhali payment channel x402 payments#91

Open
desimmons wants to merge 1 commit intoBlockRunAI:mainfrom
Dhali-org:feat/dhali-payment-channel-support
Open

(feat) Support Dhali payment channel x402 payments#91
desimmons wants to merge 1 commit intoBlockRunAI:mainfrom
Dhali-org:feat/dhali-payment-channel-support

Conversation

@desimmons
Copy link

@desimmons desimmons commented Mar 9, 2026

This PR introduces Dhali as a payment option in ClawRouter for resource servers that want to get the benefits of off-chain x402 payments:

compare-costs

Summary by CodeRabbit

  • New Features
    • Added support for Dhali payments across multiple blockchain networks (Ethereum-like chains and XRPL).
    • Expanded payment options with new fields for asset selection and payment recipient configuration.
    • Integrated optional Dhali library dependency for enhanced payment processing.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

The changes introduce Dhali payment support to the HTTP 402 payment framework. A new optional dependency on dhali-js is added, a new module creates Dhali payments across multiple networks, and the existing payment fetch mechanism integrates Dhali scheme handling alongside traditional payment flows.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Adds optional dependency for dhali-js (^3.0.4) to enable Dhali payment functionality.
Dhali Payment Module
src/dhali.ts
New module implementing createDhaliPayment function that generates Dhali payments for Ethereum-like and XRPL networks; fetches and caches remote Dhali config, handles network-specific wallet creation, and constructs base64-encoded payment headers with auth tokens.
HTTP 402 Integration
src/x402.ts
Exports PaymentOption and PaymentRequired interfaces; extends PaymentOption with maxAmountRequired, asset, and payTo fields; updates createPaymentFetch to accept unformatted private keys; adds getAccount helper for key derivation; integrates Dhali scheme support in both pre-auth and 402 retry flows.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant x402Handler as x402 Handler
    participant DhaliModule as Dhali Module
    participant ConfigService as Dhali Config Service
    participant Network as Network<br/>(Ethereum/XRPL)
    
    Client->>x402Handler: Fetch with Dhali scheme payment option
    x402Handler->>DhaliModule: createDhaliPayment(privateKey, amount, option)
    
    DhaliModule->>ConfigService: Fetch Dhali public config (cached)
    ConfigService-->>DhaliModule: Return config with protocol mappings
    
    DhaliModule->>DhaliModule: Resolve protocol & validate endpoint
    
    alt Ethereum-like Network
        DhaliModule->>Network: Create viem client for endpoint
        DhaliModule->>Network: Get auth token via DhaliChannelManager
        Network-->>DhaliModule: Return auth token
    else XRPL Network
        DhaliModule->>Network: Create XRPL Wallet & Client
        DhaliModule->>Network: Get auth token via DhaliChannelManager
        Network-->>DhaliModule: Return auth token
        DhaliModule->>Network: Disconnect
    end
    
    DhaliModule->>DhaliModule: Build base64-encoded header
    DhaliModule->>DhaliModule: Wrap auth token with header
    DhaliModule-->>x402Handler: Return wrapped payment payload
    x402Handler-->>Client: Retry request with Dhali X-Payment-Token header
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A payment protocol refined,
Where Dhali's dance and tokens bind,
Ethereum hops and XRPL sings,
Through networks wide on crypto wings,
Our 402 now sees all things! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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
Title check ✅ Passed The title clearly and concisely describes the main feature: adding Dhali payment channel support to x402 payments, which aligns with the core changes introducing createDhaliPayment function and integrating Dhali scheme into the payment flow.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 3

🧹 Nitpick comments (3)
src/x402.ts (2)

128-132: Function doesn't need to be async.

getAccount performs no asynchronous operations—privateKeyToAccount is synchronous. Making it async adds unnecessary Promise wrapping overhead.

Proposed fix
-async function getAccount(privateKey: string): Promise<{ address: `0x${string}`; privateKey: `0x${string}` }> {
+function getAccount(privateKey: string): { address: `0x${string}`; privateKey: `0x${string}` } {
   const priv = (privateKey.toLowerCase().startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
   const account = privateKeyToAccount(priv);
   return { address: account.address, privateKey: priv };
 }

Then update line 141:

-  const { address: fromAddress, privateKey: evmPrivateKey } = await getAccount(privateKey);
+  const { address: fromAddress, privateKey: evmPrivateKey } = getAccount(privateKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/x402.ts` around lines 128 - 132, The getAccount function is marked async
but calls only the synchronous privateKeyToAccount; remove the async and Promise
wrapper by changing the signature of getAccount to return the plain object type
{ address: `0x${string}`; privateKey: `0x${string}` } (not Promise<...>) and
return the value directly (leave the priv normalization and privateKeyToAccount
call intact). Update any TypeScript annotations and any call sites expecting a
Promise to use the synchronous return value if present; target the getAccount
function and the privateKeyToAccount usage when making this change.

251-263: Redundant cast of privateKey.

The cast privateKey as \0x${string}`at line 253 is unnecessary becausecreateDhaliPaymentalready handles private key normalization internally (adding0x` prefix if missing).

Proposed fix
       if (cached.scheme === "dhali") {
         paymentPayload = await createDhaliPayment(
-          privateKey as `0x${string}`,
+          privateKey,
           amount,
           {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/x402.ts` around lines 251 - 263, Remove the redundant template literal
cast of privateKey when calling createDhaliPayment: the expression `privateKey
as \`0x${string}\`` should be passed as the original variable (privateKey)
because createDhaliPayment already normalizes the key; update the call site
where cached.scheme === "dhali" so the function receives privateKey directly and
nothing else in the argument list changes (retain amount and the options
object).
src/dhali.ts (1)

79-86: Consider explicitly setting connectionTimeout on XRPL client.

While the XRPL client has a default connection timeout of 5 seconds, explicitly setting connectionTimeout makes the timeout behavior clear and allows customization if needed.

Proposed fix
-        const client = new xrpl.Client(endpointUrl);
+        const client = new xrpl.Client(endpointUrl, { connectionTimeout: 10000 });
         await client.connect();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhali.ts` around lines 79 - 86, The XRPL client is created without an
explicit connection timeout; update the xrpl.Client instantiation (new
xrpl.Client(endpointUrl)) to pass a connectionTimeout option (e.g., {
connectionTimeout: 5000 } or a configurable value) so the timeout behavior is
explicit and adjustable; keep the rest of the flow (client.connect(),
DhaliChannelManager.xrpl(wallet, client, currencyObj),
manager.getAuthToken(amount), and client.disconnect()) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 95-97: Add "xrpl" to the optionalDependencies object in
package.json alongside "dhali-js" so the dynamic import in src/dhali.ts can
succeed when XRPL support is present; update the optionalDependencies block to
include an appropriate version specifier for "xrpl" (e.g., a caret version
matching your project's supported release) so both "dhali-js" and "xrpl" are
declared as optional.

In `@src/dhali.ts`:
- Around line 16-20: The fetch of the Dhali public config (the block that
assigns cachedDhaliConfig when !cachedDhaliConfig) lacks a timeout and can hang;
update the fetch call to use an AbortController with a short timeout (e.g.
3–10s) that calls controller.abort() via setTimeout, pass controller.signal to
fetch, clear the timeout on success, and catch the abort error to throw a clear
error like "Timed out fetching Dhali public config" so cachedDhaliConfig
handling remains robust.
- Around line 54-59: The code reads result = ... then does const [symbol,
details] = result and constructs new Currency(protocol, symbol, details.scale,
details.issuer) without verifying details properties; add explicit validation
for details.scale and details.issuer (e.g., check typeof details === 'object'
and that details.scale and details.issuer are defined and of expected
types/values) before calling the Currency constructor, and if validation fails
throw a clear Error mentioning the asset and protocol (same context as the
existing missing-config error) so undefined values are never passed into
Currency.

---

Nitpick comments:
In `@src/dhali.ts`:
- Around line 79-86: The XRPL client is created without an explicit connection
timeout; update the xrpl.Client instantiation (new xrpl.Client(endpointUrl)) to
pass a connectionTimeout option (e.g., { connectionTimeout: 5000 } or a
configurable value) so the timeout behavior is explicit and adjustable; keep the
rest of the flow (client.connect(), DhaliChannelManager.xrpl(wallet, client,
currencyObj), manager.getAuthToken(amount), and client.disconnect()) unchanged.

In `@src/x402.ts`:
- Around line 128-132: The getAccount function is marked async but calls only
the synchronous privateKeyToAccount; remove the async and Promise wrapper by
changing the signature of getAccount to return the plain object type { address:
`0x${string}`; privateKey: `0x${string}` } (not Promise<...>) and return the
value directly (leave the priv normalization and privateKeyToAccount call
intact). Update any TypeScript annotations and any call sites expecting a
Promise to use the synchronous return value if present; target the getAccount
function and the privateKeyToAccount usage when making this change.
- Around line 251-263: Remove the redundant template literal cast of privateKey
when calling createDhaliPayment: the expression `privateKey as \`0x${string}\``
should be passed as the original variable (privateKey) because
createDhaliPayment already normalizes the key; update the call site where
cached.scheme === "dhali" so the function receives privateKey directly and
nothing else in the argument list changes (retain amount and the options
object).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b905ee3-2515-4c0d-9ae2-f8d0e707bf23

📥 Commits

Reviewing files that changed from the base of the PR and between 1a41e24 and 84b5af0.

📒 Files selected for processing (3)
  • package.json
  • src/dhali.ts
  • src/x402.ts

Comment on lines +95 to 97
"optionalDependencies": {
"dhali-js": "^3.0.4"
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check npm registry for package versions
echo "=== dhali-js versions ==="
npm view dhali-js versions --json 2>/dev/null | jq -r '.[-5:]' || echo "Package not found or error"

echo -e "\n=== xrpl latest version ==="
npm view xrpl version 2>/dev/null || echo "Package not found or error"

Repository: BlockRunAI/ClawRouter

Length of output: 183


🏁 Script executed:

cat src/dhali.ts

Repository: BlockRunAI/ClawRouter

Length of output: 4242


🏁 Script executed:

grep -n "xrpl" package.json

Repository: BlockRunAI/ClawRouter

Length of output: 47


🏁 Script executed:

sed -n '90,100p' package.json

Repository: BlockRunAI/ClawRouter

Length of output: 191


Add xrpl to optionalDependencies.

Both dhali-js and xrpl are dynamically imported in src/dhali.ts for XRPL network support, but only dhali-js is listed as optional. Add xrpl for consistency.

Proposed fix
  "optionalDependencies": {
    "dhali-js": "^3.0.4",
+   "xrpl": "^2.0.0"
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 95 - 97, Add "xrpl" to the optionalDependencies
object in package.json alongside "dhali-js" so the dynamic import in
src/dhali.ts can succeed when XRPL support is present; update the
optionalDependencies block to include an appropriate version specifier for
"xrpl" (e.g., a caret version matching your project's supported release) so both
"dhali-js" and "xrpl" are declared as optional.

Comment on lines +16 to +20
if (!cachedDhaliConfig) {
const response = await fetch("https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json");
if (!response.ok) throw new Error("Failed to fetch Dhali public config");
cachedDhaliConfig = await response.json();
}
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

Add timeout to config fetch to prevent indefinite hangs.

The fetch to the remote config URL has no timeout. If the GitHub raw content server is slow or unresponsive, this could block indefinitely.

Proposed fix using AbortController
     if (!cachedDhaliConfig) {
-        const response = await fetch("https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json");
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 10000);
+        try {
+            const response = await fetch(
+                "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json",
+                { signal: controller.signal }
+            );
+            if (!response.ok) throw new Error("Failed to fetch Dhali public config");
+            cachedDhaliConfig = await response.json();
+        } finally {
+            clearTimeout(timeoutId);
+        }
-        if (!response.ok) throw new Error("Failed to fetch Dhali public config");
-        cachedDhaliConfig = await response.json();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhali.ts` around lines 16 - 20, The fetch of the Dhali public config (the
block that assigns cachedDhaliConfig when !cachedDhaliConfig) lacks a timeout
and can hang; update the fetch call to use an AbortController with a short
timeout (e.g. 3–10s) that calls controller.abort() via setTimeout, pass
controller.signal to fetch, clear the timeout on success, and catch the abort
error to throw a clear error like "Timed out fetching Dhali public config" so
cachedDhaliConfig handling remains robust.

Comment on lines +54 to +59
if (!result) {
throw new Error(`Dhali configuration missing currency metadata for asset: ${option.asset} on protocol: ${protocol}`);
}
const [symbol, details] = result;

const currencyObj = new Currency(protocol, symbol, details.scale, details.issuer);
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

Validate details.scale and details.issuer before use.

If the Dhali config has incomplete currency metadata, accessing details.scale or details.issuer could pass undefined to the Currency constructor, potentially causing runtime errors or unexpected behavior.

Proposed validation
     if (!result) {
         throw new Error(`Dhali configuration missing currency metadata for asset: ${option.asset} on protocol: ${protocol}`);
     }
     const [symbol, details] = result;
+    if (details.scale === undefined) {
+        throw new Error(`Dhali configuration missing scale for asset: ${option.asset}`);
+    }

     const currencyObj = new Currency(protocol, symbol, details.scale, details.issuer);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhali.ts` around lines 54 - 59, The code reads result = ... then does
const [symbol, details] = result and constructs new Currency(protocol, symbol,
details.scale, details.issuer) without verifying details properties; add
explicit validation for details.scale and details.issuer (e.g., check typeof
details === 'object' and that details.scale and details.issuer are defined and
of expected types/values) before calling the Currency constructor, and if
validation fails throw a clear Error mentioning the asset and protocol (same
context as the existing missing-config error) so undefined values are never
passed into Currency.

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