-
Notifications
You must be signed in to change notification settings - Fork 409
(feat) Support Dhali payment channel x402 payments #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,5 +91,8 @@ | |
| }, | ||
| "engines": { | ||
| "node": ">=20" | ||
| }, | ||
| "optionalDependencies": { | ||
| "dhali-js": "^3.0.4" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { PaymentOption } from "./x402.js"; | ||
| import { createWalletClient, createPublicClient, http } from "viem"; | ||
| import { privateKeyToAccount } from "viem/accounts"; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| let cachedDhaliConfig: Record<string, any> | null = null; | ||
|
|
||
| export async function createDhaliPayment( | ||
| privateKey: string, | ||
| amount: string, | ||
| option: PaymentOption, | ||
| paymentHeader?: string | ||
| ): Promise<string> { | ||
| const networkCode = option.network.toLowerCase(); | ||
|
|
||
| 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(); | ||
| } | ||
|
Comment on lines
+16
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
|
|
||
| const [protocol] = Object.entries(cachedDhaliConfig?.CAIP_2_MAPPINGS || {}).find( | ||
| ([, c]) => typeof c === "string" && c.toLowerCase() === networkCode | ||
| ) || []; | ||
|
|
||
| if (!protocol) { | ||
| throw new Error(`Dhali configuration missing protocol mapping for CAIP-2 network: ${option.network}`); | ||
| } | ||
|
|
||
| const isXrpl = protocol.startsWith("XRPL") || protocol.startsWith("XAHL"); | ||
| const isEthereum = protocol === "ETHEREUM" || protocol === "SEPOLIA"; | ||
|
|
||
| const endpointUrl = cachedDhaliConfig?.PUBLIC_CLIENTS?.[protocol]?.HTTP_CLIENT; | ||
| if (!endpointUrl) throw new Error(`Missing endpoint for protocol ${protocol}`); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| let dhaliObj: any; | ||
| try { | ||
| // @ts-expect-error - dhali-js is not typed | ||
| dhaliObj = await import("dhali-js"); | ||
| } catch { | ||
| throw new Error("dhali-js is required for Dhali payments. please 'npm install dhali-js'."); | ||
| } | ||
| const { DhaliChannelManager, wrapAsX402PaymentPayload, Currency } = dhaliObj.default || dhaliObj; | ||
|
|
||
| const currencyMetadata = cachedDhaliConfig?.DHALI_PUBLIC_ADDRESSES?.[protocol] || {}; | ||
| const assetLower = option.asset.toLowerCase(); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const result = (Object.entries(currencyMetadata) as [string, any][]).find( | ||
| ([, d]) => d.caip19?.toLowerCase() === assetLower | ||
| ); | ||
|
|
||
| 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); | ||
|
Comment on lines
+54
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate If the Dhali config has incomplete currency metadata, accessing 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 |
||
|
|
||
| let claim: string; | ||
| if (isEthereum) { | ||
| const finalPriv = (privateKey.toLowerCase().startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`; | ||
| const account = privateKeyToAccount(finalPriv); | ||
| const publicClient = createPublicClient({ transport: http(endpointUrl) }); | ||
| const walletClient = createWalletClient({ account, transport: http(endpointUrl) }); | ||
| const manager = DhaliChannelManager.evm(walletClient, publicClient, currencyObj); | ||
| claim = await manager.getAuthToken(amount); | ||
| } else if (isXrpl) { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| let xrpl: any; | ||
| try { | ||
| xrpl = await import("xrpl"); | ||
| } catch { | ||
| throw new Error("xrpl is required for XRPL payments. please 'npm install xrpl'."); | ||
| } | ||
|
|
||
| const wallet = xrpl.Wallet.fromSeed(privateKey); | ||
| const client = new xrpl.Client(endpointUrl); | ||
| await client.connect(); | ||
| try { | ||
| const manager = DhaliChannelManager.xrpl(wallet, client, currencyObj); | ||
| claim = await manager.getAuthToken(amount); | ||
| } finally { | ||
| await client.disconnect(); | ||
| } | ||
| } else { | ||
| throw new Error(`Unsupported network: ${option.network}`); | ||
| } | ||
|
|
||
| const headerToWrap = paymentHeader || Buffer.from(JSON.stringify({ | ||
| scheme: option.scheme, | ||
| network: option.network, | ||
| asset: option.asset, | ||
| payTo: option.payTo, | ||
| amount: amount.toString(), | ||
| maxTimeoutSeconds: option.maxTimeoutSeconds, | ||
| extra: option.extra || {}, | ||
| })).toString("base64"); | ||
| return wrapAsX402PaymentPayload(claim, headerToWrap); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: BlockRunAI/ClawRouter
Length of output: 183
🏁 Script executed:
Repository: BlockRunAI/ClawRouter
Length of output: 4242
🏁 Script executed:
grep -n "xrpl" package.jsonRepository: BlockRunAI/ClawRouter
Length of output: 47
🏁 Script executed:
sed -n '90,100p' package.jsonRepository: BlockRunAI/ClawRouter
Length of output: 191
Add
xrplto optionalDependencies.Both
dhali-jsandxrplare dynamically imported insrc/dhali.tsfor XRPL network support, but onlydhali-jsis listed as optional. Addxrplfor consistency.Proposed fix
"optionalDependencies": { "dhali-js": "^3.0.4", + "xrpl": "^2.0.0" }🤖 Prompt for AI Agents