-
Notifications
You must be signed in to change notification settings - Fork 653
fix(proxy): invoke onPayment after confirmed settlement (rebase of #325) #330
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
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 |
|---|---|---|
|
|
@@ -47,11 +47,17 @@ const DEFAULT_TTL_MS = 3_600_000; // 1 hour | |
|
|
||
| type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>; | ||
|
|
||
| export type PaymentNotification = { model: string; amount: string; network: string }; | ||
|
|
||
| export function createPayFetchWithPreAuth( | ||
| baseFetch: FetchFn, | ||
| client: x402Client, | ||
| ttlMs = DEFAULT_TTL_MS, | ||
| options?: { skipPreAuth?: boolean; estimateAmount?: EstimateFn }, | ||
| options?: { | ||
| skipPreAuth?: boolean; | ||
| estimateAmount?: EstimateFn; | ||
| onPayment?: (info: PaymentNotification) => void; | ||
| }, | ||
| ): FetchFn { | ||
| const httpClient = new x402HTTPClient(client); | ||
| const cache = new Map<string, CachedEntry>(); | ||
|
|
@@ -92,6 +98,28 @@ export function createPayFetchWithPreAuth( | |
| } | ||
| const cacheKey = `${urlPath}:${requestModel}`; | ||
|
|
||
| const notifyAcceptedPayment = ( | ||
| response: Response, | ||
| payload: { accepted: { amount: string; network: string } }, | ||
| ): void => { | ||
| const settled = | ||
| response.headers.has("payment-response") || response.headers.has("x-payment-response"); | ||
| if (!settled || !options?.onPayment) return; | ||
| try { | ||
| options.onPayment({ | ||
| model: requestModel, | ||
|
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- src/payment-preauth.ts (relevant range) ---'
cat -n src/payment-preauth.ts | sed -n '1,190p'
printf '%s\n' '--- FetchFn and payment-preauth call sites ---'
rg -n -C 4 'FetchFn|payment-preauth|notifyAcceptedPayment|requestModel|new Request' src test tests 2>/dev/null || true
printf '%s\n' '--- candidate type and test files ---'
git ls-files | rg '(^|/)(payment-preauth|.*payment.*test|.*preauth.*test|.*types?).*\.(ts|tsx|js|jsx)$' || trueRepository: BlockRunAI/ClawRouter Length of output: 20505 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- remaining payment-preauth flow ---'
cat -n src/payment-preauth.ts | sed -n '185,245p'
printf '%s\n' '--- notification-related tests and request-input coverage ---'
rg -n -C 8 'onPayment|PaymentNotification|payment-response|x-payment-response|new Request\\(' src/payment-preauth.test.tsRepository: BlockRunAI/ClawRouter Length of output: 2703 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- remaining payment-preauth flow ---'
cat -n src/payment-preauth.ts | sed -n '185,245p'
printf '%s\n' '--- notification-related tests and request-input coverage ---'
rg -n -C 8 'onPayment|PaymentNotification|payment-response|x-payment-response|new Request\(' src/payment-preauth.test.tsRepository: BlockRunAI/ClawRouter Length of output: 6020 Parse the request body for
🤖 Prompt for AI Agents |
||
| amount: payload.accepted.amount, | ||
| network: payload.accepted.network, | ||
| }); | ||
| } catch (error) { | ||
| // The observer runs after settlement. Its failure must not turn a paid | ||
| // response into a retryable request and risk a second charge. | ||
| console.error( | ||
| `[ClawRouter] onPayment callback failed: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| // Up-front estimate of what THIS request will cost (USDC micro-units), used | ||
| // both to gate pre-auth reuse and to record what a new cache entry covers. | ||
| const estimateMicros = (): number | undefined => { | ||
|
|
@@ -132,6 +160,7 @@ export function createPayFetchWithPreAuth( | |
| paymentInFlight = true; | ||
| const response = await baseFetch(preAuthRequest); | ||
| if (response.status !== 402) { | ||
| notifyAcceptedPayment(response, payload); | ||
| return response; // Pre-auth worked — saved ~200ms | ||
| } | ||
| // Rejected despite our estimate (server priced it higher than we did). | ||
|
|
@@ -200,6 +229,8 @@ export function createPayFetchWithPreAuth( | |
| for (const [key, value] of Object.entries(paymentHeaders)) { | ||
| clonedRequest.headers.set(key, value); | ||
| } | ||
| return baseFetch(clonedRequest); | ||
| const paidResponse = await baseFetch(clonedRequest); | ||
| notifyAcceptedPayment(paidResponse, payload); | ||
| return paidResponse; | ||
| }; | ||
| } | ||
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.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: BlockRunAI/ClawRouter
Length of output: 18154
🏁 Script executed:
Repository: BlockRunAI/ClawRouter
Length of output: 5738
Handle rejected asynchronous observers.
The synchronous
try/catchinnotifyAcceptedPaymentdoes not handle a rejected Promise from anasynconPaymentobserver. The ignored rejection can become unhandled after settlement. Change both callback contracts tovoid | Promise<void>, attach a non-awaited.catch(...), and add a test for an asynchronously rejected observer.🤖 Prompt for AI Agents