-
Notifications
You must be signed in to change notification settings - Fork 1
Add Para as a WalletProvider (ENG-1976) #14
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 11 commits
260a0b6
a31528a
5eb741c
fc1173d
698c967
c56c71d
2ae67dc
44540c3
a5d63fd
66b680c
8aa34d0
6ba57c2
d438518
d0716c0
3c0fd7c
ba01604
2c2ea77
2f858f4
b413f37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import type { Command } from 'commander'; | ||
| import { confirm } from '@inquirer/prompts'; | ||
| import { formatUnits, parseUnits, type Address } from 'viem'; | ||
| import { formatUnits, isAddress, parseUnits, type Address } from 'viem'; | ||
| import { resolveConfig } from '../lib/config.js'; | ||
| import { requireAccount } from '../lib/account.js'; | ||
| import { makePublicClient } from '../lib/client.js'; | ||
|
|
@@ -20,6 +20,7 @@ import { | |
| import { | ||
| decodePaymentResponse, | ||
| encodePaymentHeader, | ||
| encodePermitPaymentHeader, | ||
| networkIdForChain, | ||
| parseChallenge, | ||
| pickAccept, | ||
|
|
@@ -32,6 +33,13 @@ import { | |
| readBalance, | ||
| signTransferAuthorization, | ||
| } from '../lib/x402/eip3009.js'; | ||
| import { readPermitNonce, signEip2612Permit, signPermit } from '../lib/x402/eip2612.js'; | ||
| import { | ||
| buildPermit2PaymentPayload, | ||
| PERMIT2_ADDRESS, | ||
| randomPermit2Nonce, | ||
| signPermit2WitnessTransfer, | ||
| } from '../lib/x402/permit2.js'; | ||
| import type { GlobalOptions } from '../types.js'; | ||
|
|
||
| interface SubOptions { | ||
|
|
@@ -106,13 +114,20 @@ async function runX402( | |
| } | ||
|
|
||
| const cfg = resolveConfig(opts); | ||
| // Official Radius x402 v2 servers carry the challenge in a base64 | ||
| // PAYMENT-REQUIRED response header; older servers put JSON in the body. | ||
| let challenge; | ||
| const paymentRequired = initial.headers.get('payment-required'); | ||
| try { | ||
| const text = decodeBodyAsUtf8(initial.body) ?? ''; | ||
| challenge = parseChallenge(JSON.parse(text)); | ||
| if (paymentRequired) { | ||
| challenge = parseChallenge(JSON.parse(Buffer.from(paymentRequired, 'base64').toString('utf8'))); | ||
| } else { | ||
| const text = decodeBodyAsUtf8(initial.body) ?? ''; | ||
| challenge = parseChallenge(JSON.parse(text)); | ||
| } | ||
| } catch (e) { | ||
| process.stderr.write( | ||
| `x402: server returned 402 but the body is not a valid challenge: ${(e as Error).message}\n`, | ||
| `x402: server returned 402 but no valid challenge in ${paymentRequired ? 'PAYMENT-REQUIRED header' : 'body'}: ${(e as Error).message}\n`, | ||
| ); | ||
| process.stderr.write(safeBodyPreview(initial.body)); | ||
| process.exit(2); | ||
|
|
@@ -170,38 +185,136 @@ async function runX402( | |
| } | ||
| } | ||
|
|
||
| const authorization = makeAuthorization({ | ||
| from: account.address, | ||
| to: accept.payTo, | ||
| value: accept.maxAmountRequired, | ||
| maxTimeoutSeconds: accept.maxTimeoutSeconds, | ||
| }); | ||
| // Radius servers settle SBC via EIP-2612 permit + transferFrom (SBC has no | ||
| // EIP-3009) and advertise it in the challenge's extra. Standard x402 servers | ||
| // get the EIP-3009 transferWithAuthorization path. | ||
| const settlementSpender = | ||
| accept.extra?.settlementMethod === 'permit-transferFrom' && | ||
| typeof accept.extra.settlementSpender === 'string' && | ||
| isAddress(accept.extra.settlementSpender) | ||
| ? (accept.extra.settlementSpender as Address) | ||
| : undefined; | ||
|
|
||
| let signature; | ||
| try { | ||
| signature = await signTransferAuthorization(account, { | ||
| asset: accept.asset, | ||
| chainId: cfg.chain.id, | ||
| name: asset.name, | ||
| version: asset.version, | ||
| authorization, | ||
| // Official Radius v2 settlement: Permit2 dual-signature flow. | ||
| const usePermit2 = accept.extra?.assetTransferMethod === 'permit2'; | ||
|
|
||
| let paymentHeader: string; | ||
| if (usePermit2) { | ||
| try { | ||
| const eip2612Nonce = await readPermitNonce(client, accept.asset, account.address); | ||
| const deadline = BigInt( | ||
| Math.floor(Date.now() / 1000) + (accept.maxTimeoutSeconds ?? 300), | ||
| ); | ||
| const eip2612Signature = await signEip2612Permit(account, { | ||
| asset: accept.asset, | ||
| chainId: cfg.chain.id, | ||
| name: asset.name, | ||
| version: asset.version, | ||
| spender: PERMIT2_ADDRESS, | ||
| value: accept.maxAmountRequired, | ||
| nonce: eip2612Nonce, | ||
| deadline, | ||
| }); | ||
| const permit2Nonce = randomPermit2Nonce(); | ||
| const permit2Signature = await signPermit2WitnessTransfer(account, { | ||
| token: accept.asset, | ||
| chainId: cfg.chain.id, | ||
| amount: accept.maxAmountRequired, | ||
| payTo: accept.payTo, | ||
| nonce: permit2Nonce, | ||
| deadline, | ||
| }); | ||
| const challengeResource = | ||
| challenge.resource && typeof challenge.resource === 'object' | ||
| ? (challenge.resource as { url?: string; description?: string; mimeType?: string }) | ||
| : {}; | ||
| paymentHeader = buildPermit2PaymentPayload({ | ||
| chainId: cfg.chain.id, | ||
| resource: { ...challengeResource, url: challengeResource.url ?? url }, | ||
| accepted: accept.raw, | ||
| token: accept.asset, | ||
| amount: accept.maxAmountRequired, | ||
| owner: account.address, | ||
| payTo: accept.payTo, | ||
| permit2Signature, | ||
| permit2Nonce, | ||
| eip2612Signature, | ||
| eip2612Nonce, | ||
| deadline, | ||
| }); | ||
| } catch (e) { | ||
| process.stderr.write( | ||
| `x402: failed to sign permit2 payment: ${(e as Error).message}\n`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| } else if (settlementSpender) { | ||
| try { | ||
| const nonce = await readPermitNonce(client, accept.asset, account.address); | ||
| const deadline = BigInt( | ||
| Math.floor(Date.now() / 1000) + (accept.maxTimeoutSeconds ?? 300), | ||
| ); | ||
| const permit = await signPermit(account, { | ||
| asset: accept.asset, | ||
| chainId: cfg.chain.id, | ||
| name: asset.name, | ||
| version: asset.version, | ||
| spender: settlementSpender, | ||
| value: accept.maxAmountRequired, | ||
| nonce, | ||
| deadline, | ||
| }); | ||
| paymentHeader = encodePermitPaymentHeader({ | ||
| x402Version: challenge.x402Version, | ||
| scheme: accept.scheme, | ||
| network: accept.network, | ||
| payload: permit, | ||
| }); | ||
| } catch (e) { | ||
| process.stderr.write( | ||
| `x402: failed to sign EIP-2612 permit: ${(e as Error).message}\n`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| } else { | ||
| const authorization = makeAuthorization({ | ||
| from: account.address, | ||
| to: accept.payTo, | ||
| value: accept.maxAmountRequired, | ||
| maxTimeoutSeconds: accept.maxTimeoutSeconds, | ||
| }); | ||
| } catch (e) { | ||
| process.stderr.write( | ||
| `x402: failed to sign EIP-3009 authorization (asset may not support transferWithAuthorization): ${(e as Error).message}\n`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const paymentHeader = encodePaymentHeader({ | ||
| x402Version: challenge.x402Version, | ||
| scheme: accept.scheme, | ||
| network: accept.network, | ||
| payload: { signature, authorization }, | ||
| }); | ||
| let signature; | ||
| try { | ||
| signature = await signTransferAuthorization(account, { | ||
| asset: accept.asset, | ||
| chainId: cfg.chain.id, | ||
| name: asset.name, | ||
| version: asset.version, | ||
| authorization, | ||
| }); | ||
| } catch (e) { | ||
| process.stderr.write( | ||
| `x402: failed to sign EIP-3009 authorization (asset may not support transferWithAuthorization): ${(e as Error).message}\n`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| paymentHeader = encodePaymentHeader({ | ||
| x402Version: challenge.x402Version, | ||
| scheme: accept.scheme, | ||
| network: accept.network, | ||
| accepted: accept.raw, | ||
| resource: challenge.resource, | ||
| payload: { signature, authorization }, | ||
| }); | ||
| } | ||
|
|
||
| const retryHeaders = new Headers(reqHeaders); | ||
| // Send both header spellings: official Radius v2 servers read | ||
| // PAYMENT-SIGNATURE, older x402 servers read X-Payment. | ||
| retryHeaders.set('x-payment', paymentHeader); | ||
| retryHeaders.set('payment-signature', paymentHeader); | ||
|
|
||
| const retry = await runRequest(verb as HttpVerb, url, { | ||
| headers: retryHeaders, | ||
|
|
@@ -220,7 +333,7 @@ async function runX402( | |
| } | ||
|
|
||
| let paymentResponse: PaymentResponseBody | null = null; | ||
| const xpr = retry.headers.get('x-payment-response'); | ||
| const xpr = retry.headers.get('payment-response') ?? retry.headers.get('x-payment-response'); | ||
|
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. While adding official |
||
| if (xpr) { | ||
| try { paymentResponse = decodePaymentResponse(xpr); } catch { /* ignore malformed */ } | ||
| } | ||
|
|
||
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.
Since this PR now sends
payment-signature, we should also filter user-suppliedpayment-signatureheaders on the initial request.parseHeaderArgscurrently blocksx-paymentbut not this spelling, so a stale or mismatched payment could be sent before the challenge/retry flow.