@absol-labs/agent 0.12.0-rc.1 → 0.12.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/commands/claim.d.ts +32 -0
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +115 -27
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +15 -0
- package/dist/cli/commands/pay.d.ts.map +1 -1
- package/dist/cli/commands/pay.js +131 -7
- package/dist/cli/commands/pay.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +20 -4
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +19 -1
- package/dist/cli/commands/refund.d.ts.map +1 -1
- package/dist/cli/commands/refund.js +200 -27
- package/dist/cli/commands/refund.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +11 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -0
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/context.d.ts +19 -0
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/deps.d.ts +20 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +14 -0
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/progress.d.ts +13 -1
- package/dist/cli/progress.d.ts.map +1 -1
- package/dist/cli/progress.js +7 -1
- package/dist/cli/progress.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +26 -14
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +103 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +310 -41
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +1 -1
- package/dist/cli/wallet.d.ts.map +1 -1
- package/dist/cli/wallet.js +28 -4
- package/dist/cli/wallet.js.map +1 -1
- package/dist/gateway/http-server.d.ts +17 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +76 -9
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/x402-opener-mode.d.ts +39 -2
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -1
- package/dist/gateway/x402-opener-mode.js +169 -36
- package/dist/gateway/x402-opener-mode.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +28 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +8 -3
- package/dist/mcp/x402-tools.d.ts.map +1 -1
- package/dist/mcp/x402-tools.js +126 -6
- package/dist/mcp/x402-tools.js.map +1 -1
- package/dist/sdk/client.d.ts +13 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +15 -0
- package/dist/sdk/client.js.map +1 -1
- package/dist/x402-buyer/pay.d.ts +22 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -1
- package/dist/x402-buyer/pay.js +33 -3
- package/dist/x402-buyer/pay.js.map +1 -1
- package/dist/x402-opener/facilitator-entry.js +4 -1
- package/dist/x402-opener/facilitator-entry.js.map +1 -1
- package/dist/x402-opener/facilitator-server.d.ts +29 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -1
- package/dist/x402-opener/facilitator-server.js +67 -1
- package/dist/x402-opener/facilitator-server.js.map +1 -1
- package/dist/x402-opener/refund-relay.d.ts +30 -4
- package/dist/x402-opener/refund-relay.d.ts.map +1 -1
- package/dist/x402-opener/refund-relay.js +123 -22
- package/dist/x402-opener/refund-relay.js.map +1 -1
- package/dist/x402-opener/scheme.d.ts +79 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -1
- package/dist/x402-opener/scheme.js +326 -40
- package/dist/x402-opener/scheme.js.map +1 -1
- package/dist/x402-opener/terms.d.ts +61 -1
- package/dist/x402-opener/terms.d.ts.map +1 -1
- package/dist/x402-opener/terms.js +94 -2
- package/dist/x402-opener/terms.js.map +1 -1
- package/docs/cli.md +87 -9
- package/docs/mcp-reference.md +18 -2
- package/docs/quickstart.md +3 -3
- package/docs/x402.md +150 -10
- package/package.json +3 -3
- package/src/cli/commands/claim.ts +167 -32
- package/src/cli/commands/pay.ts +150 -8
- package/src/cli/commands/reclaim.ts +25 -5
- package/src/cli/commands/refund.ts +231 -33
- package/src/cli/commands/status.ts +11 -1
- package/src/cli/commands/watch.ts +4 -0
- package/src/cli/context.ts +16 -0
- package/src/cli/deps.ts +37 -0
- package/src/cli/progress.ts +21 -2
- package/src/cli/run.ts +26 -13
- package/src/cli/status.ts +440 -43
- package/src/cli/wallet.ts +44 -7
- package/src/gateway/http-server.ts +96 -8
- package/src/gateway/x402-opener-mode.ts +234 -37
- package/src/mcp/server.ts +43 -11
- package/src/mcp/x402-tools.ts +171 -7
- package/src/sdk/client.ts +25 -0
- package/src/x402-buyer/pay.ts +41 -3
- package/src/x402-opener/facilitator-entry.ts +4 -1
- package/src/x402-opener/facilitator-server.ts +101 -1
- package/src/x402-opener/refund-relay.ts +150 -20
- package/src/x402-opener/scheme.ts +459 -41
- package/src/x402-opener/terms.ts +155 -2
package/src/mcp/x402-tools.ts
CHANGED
|
@@ -12,7 +12,13 @@ import {
|
|
|
12
12
|
verifyMandateOwnerSignature,
|
|
13
13
|
type SignedSpendMandate,
|
|
14
14
|
} from "../mandates/mandate.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createPublicClient,
|
|
17
|
+
http,
|
|
18
|
+
keccak256,
|
|
19
|
+
toHex,
|
|
20
|
+
type PublicClient,
|
|
21
|
+
} from "viem";
|
|
16
22
|
|
|
17
23
|
import {
|
|
18
24
|
assertOutboundUrlAllowed,
|
|
@@ -39,11 +45,11 @@ import type {
|
|
|
39
45
|
*/
|
|
40
46
|
|
|
41
47
|
export const X402_PAY_TOOL_DESCRIPTION =
|
|
42
|
-
"Pay an x402 endpoint (HTTP 402 Payment Required, x402 v2 'exact') and return its response. A Metrik x402 seller quotes a payment that opens an escrow STREAM
|
|
48
|
+
"Pay an x402 endpoint (HTTP 402 Payment Required, x402 v2 'exact') and return its response. A Metrik x402 seller quotes a payment that opens an escrow STREAM; the seller earns ratePerSecond x VERIFIED seconds, capped at the deposit, and the unearned remainder stays recoverable by this server's wallet. An UPFRONT ('minimum charge') quote splits the price: upfrontAmount is a NON-REFUNDABLE per-call charge paid to the seller at settlement, and only price - upfrontAmount is escrowed and refundable; when upfrontAmount == price no stream opens at all (streamId null, nothing to refund). Subject to the agent's spend mandate (the WHOLE price vs per-stream and total caps, rate, duration, operator) and a cap on the non-refundable part (maxUpfrontAmountUsdc, default 0.10 USDC), checked BEFORE anything is signed. Refunds are NOT automatic: use refund_status and refund with the returned streamId.";
|
|
43
49
|
export const REFUND_STATUS_TOOL_DESCRIPTION =
|
|
44
|
-
"Read the refund state of a stream this server paid for over x402, from the seller's gasless refund relay: whether it is running, what is reclaimable, and the next step (close / wait / reclaim). Moves no funds.";
|
|
50
|
+
"Read the refund state of a stream this server paid for over x402, from the seller's gasless refund relay: whether it is running, what is reclaimable, and the next step (close / wait / reclaim). An upfront-only payment (no stream) has nothing to refund. Moves no funds.";
|
|
45
51
|
export const REFUND_TOOL_DESCRIPTION =
|
|
46
|
-
"Recover the unearned part of an x402 payment through the seller's gasless refund relay: close=true stops the stream (this wallet signs the opener's Close; the relay pays the gas), reclaim=true sends the permissionless reclaim (the refund always goes to the payer). Nothing is refunded automatically; reclaim waits for the settlement window and may say when to retry.";
|
|
52
|
+
"Recover the unearned part of an x402 payment through the seller's gasless refund relay: close=true stops the stream (this wallet signs the opener's Close; the relay pays the gas), reclaim=true sends the permissionless reclaim (the refund always goes to the payer). Only the escrowed part can come back: an upfront charge is final, and an upfront-only payment has nothing to refund. Nothing is refunded automatically; reclaim waits for the settlement window and may say when to retry.";
|
|
47
53
|
|
|
48
54
|
export interface McpX402Options {
|
|
49
55
|
/** The payer: this server's own wallet (must own the server mandate). */
|
|
@@ -108,12 +114,17 @@ export interface McpX402Context {
|
|
|
108
114
|
readonly refundUrl?: string;
|
|
109
115
|
readonly opener?: `0x${string}`;
|
|
110
116
|
readonly x402Reservation?: boolean;
|
|
117
|
+
/** Not a stream: a settled upfront-only charge (counted spend, nothing to refund). */
|
|
118
|
+
readonly x402Charge?: "upfront-only";
|
|
119
|
+
/** The non-refundable part of the payment (the upfront charge), base units. */
|
|
120
|
+
readonly nonRefundableUsdc?: bigint;
|
|
111
121
|
}) => Promise<void>;
|
|
112
122
|
/** Drop a record (a released reservation). Absent = reservations are kept. */
|
|
113
123
|
readonly removeStream?: (streamId: `0x${string}`) => Promise<void>;
|
|
114
124
|
readonly findStream: (streamId: `0x${string}`) => Promise<{
|
|
115
125
|
readonly refundUrl?: string;
|
|
116
126
|
readonly opener?: `0x${string}`;
|
|
127
|
+
readonly x402Charge?: "upfront-only";
|
|
117
128
|
} | null>;
|
|
118
129
|
readonly now: () => number;
|
|
119
130
|
readonly ok: (
|
|
@@ -167,8 +178,17 @@ const x402PaySchema = {
|
|
|
167
178
|
.describe(
|
|
168
179
|
"Refuse a quote the seller can fully earn in fewer verified seconds.",
|
|
169
180
|
),
|
|
181
|
+
maxUpfrontAmountUsdc: z
|
|
182
|
+
.string()
|
|
183
|
+
.regex(/^(0|[1-9]\d*)$/, "a non-negative integer of USDC base units")
|
|
184
|
+
.optional()
|
|
185
|
+
.describe(
|
|
186
|
+
"Cap on the NON-REFUNDABLE per-call part of an upfront-opener quote, base units. Default 100000 (0.10 USDC); 0 refuses any upfront charge.",
|
|
187
|
+
),
|
|
170
188
|
} as const;
|
|
171
189
|
|
|
190
|
+
const ZERO_STREAM_ID = `0x${"00".repeat(32)}` as const;
|
|
191
|
+
|
|
172
192
|
const refundSchema = {
|
|
173
193
|
streamId: bytes32Schema,
|
|
174
194
|
refundUrl: z
|
|
@@ -232,6 +252,11 @@ export function registerX402Tools(
|
|
|
232
252
|
...(target.opener === undefined ? {} : { opener: target.opener }),
|
|
233
253
|
...(target.fetch === undefined ? {} : { fetch: target.fetch }),
|
|
234
254
|
expectedChainId: ctx.x402.network.chain.id,
|
|
255
|
+
// Chain access is what lets the SDK read the opener's KIND on-chain and
|
|
256
|
+
// sign an upfront opener's Close under its own EIP-712 domain; without
|
|
257
|
+
// it, such a Close is refused.
|
|
258
|
+
publicClient: refundReader(ctx.x402),
|
|
259
|
+
network: ctx.x402.network,
|
|
235
260
|
});
|
|
236
261
|
});
|
|
237
262
|
|
|
@@ -304,6 +329,9 @@ export function registerX402Tools(
|
|
|
304
329
|
...(args.minDurationSeconds === undefined
|
|
305
330
|
? {}
|
|
306
331
|
: { minDurationSeconds: args.minDurationSeconds }),
|
|
332
|
+
...(args.maxUpfrontAmountUsdc === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: { maxUpfrontAmount: BigInt(args.maxUpfrontAmountUsdc) }),
|
|
307
335
|
sdkOptions: { ...ctx.x402.sdkOptions, fetch: guardedFetch },
|
|
308
336
|
...(ctx.x402.chainReader === undefined
|
|
309
337
|
? {}
|
|
@@ -327,6 +355,9 @@ export function registerX402Tools(
|
|
|
327
355
|
mandateId: signedMandate.mandateId as `0x${string}`,
|
|
328
356
|
opener: quote.payTo,
|
|
329
357
|
x402Reservation: true,
|
|
358
|
+
...(quote.upfrontAmount === null || quote.upfrontAmount === 0n
|
|
359
|
+
? {}
|
|
360
|
+
: { nonRefundableUsdc: quote.upfrontAmount }),
|
|
330
361
|
});
|
|
331
362
|
},
|
|
332
363
|
});
|
|
@@ -340,7 +371,38 @@ export function registerX402Tools(
|
|
|
340
371
|
});
|
|
341
372
|
}
|
|
342
373
|
const opener = result.metrikStreamOpener;
|
|
343
|
-
|
|
374
|
+
const upfrontAmount = result.quote?.upfrontAmount ?? null;
|
|
375
|
+
if (
|
|
376
|
+
result.paid &&
|
|
377
|
+
opener !== null &&
|
|
378
|
+
opener.streamId === null &&
|
|
379
|
+
result.approved !== null
|
|
380
|
+
) {
|
|
381
|
+
// Upfront-only: there is NO stream to record or refund. The spend
|
|
382
|
+
// still counts against the mandate (the whole price is gone), so
|
|
383
|
+
// the reservation is replaced by a permanent, non-stream charge.
|
|
384
|
+
await ctx.recordStream({
|
|
385
|
+
streamId: chargeKey(owner, opener.opener, result),
|
|
386
|
+
buyer: owner,
|
|
387
|
+
operator: result.approved.operator,
|
|
388
|
+
serviceRef:
|
|
389
|
+
result.quote?.serviceRef ?? (`0x${"00".repeat(32)}` as const),
|
|
390
|
+
budgetUsdc: result.approved.budgetUsdc,
|
|
391
|
+
ratePerSecondUsdc: result.approved.ratePerSecondUsdc,
|
|
392
|
+
maxDurationSeconds: result.approved.maxDurationSeconds,
|
|
393
|
+
openedAtSeconds: nowSeconds,
|
|
394
|
+
mandateId: signedMandate.mandateId as `0x${string}`,
|
|
395
|
+
opener: opener.opener,
|
|
396
|
+
x402Charge: "upfront-only",
|
|
397
|
+
nonRefundableUsdc: result.approved.budgetUsdc,
|
|
398
|
+
});
|
|
399
|
+
await release();
|
|
400
|
+
} else if (
|
|
401
|
+
result.paid &&
|
|
402
|
+
opener !== null &&
|
|
403
|
+
opener.streamId !== null &&
|
|
404
|
+
result.approved !== null
|
|
405
|
+
) {
|
|
344
406
|
// Replace the reservation with the real stream (same amount).
|
|
345
407
|
await ctx.recordStream({
|
|
346
408
|
streamId: opener.streamId,
|
|
@@ -357,6 +419,9 @@ export function registerX402Tools(
|
|
|
357
419
|
? {}
|
|
358
420
|
: { refundUrl: opener.refundUrl }),
|
|
359
421
|
opener: opener.opener,
|
|
422
|
+
...(upfrontAmount === null || upfrontAmount === 0n
|
|
423
|
+
? {}
|
|
424
|
+
: { nonRefundableUsdc: upfrontAmount }),
|
|
360
425
|
});
|
|
361
426
|
await release();
|
|
362
427
|
} else if (!result.paid) {
|
|
@@ -386,10 +451,26 @@ export function registerX402Tools(
|
|
|
386
451
|
ratePerSecondUsdc: opener?.ratePerSecond?.toString() ?? null,
|
|
387
452
|
expiresAt: opener?.expiresAt ?? null,
|
|
388
453
|
minEarnSeconds: result.quote?.minEarnSeconds?.toString() ?? null,
|
|
454
|
+
openerKind: result.quote?.openerKind ?? null,
|
|
455
|
+
priceUsdc: result.quote?.amount.toString() ?? null,
|
|
456
|
+
nonRefundableUsdc: upfrontAmount?.toString() ?? null,
|
|
457
|
+
refundableUsdc: result.quote?.refundableAmount?.toString() ?? null,
|
|
458
|
+
upfrontFeeUsdc: opener?.upfrontFee?.toString() ?? null,
|
|
459
|
+
upfrontOnly: opener?.upfrontOnly ?? false,
|
|
389
460
|
refundIsAutomatic: false,
|
|
390
461
|
};
|
|
462
|
+
if (opener !== null && opener.streamId === null) {
|
|
463
|
+
return ctx.ok(
|
|
464
|
+
`paid ${result.quote?.amount.toString() ?? "?"} USDC base units as an UPFRONT-ONLY charge; HTTP ${result.response.status}. The whole price was a non-refundable per-call charge paid to the seller at settlement: no stream was opened, the payment is final and there is nothing to refund.`,
|
|
465
|
+
structured,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const split =
|
|
469
|
+
upfrontAmount !== null && upfrontAmount > 0n
|
|
470
|
+
? ` Of the ${result.quote?.amount.toString() ?? "?"} paid, ${upfrontAmount.toString()} was a NON-REFUNDABLE per-call charge (final); only the ${opener?.deposit?.toString() ?? "?"} escrowed can come back.`
|
|
471
|
+
: "";
|
|
391
472
|
return ctx.ok(
|
|
392
|
-
`paid ${opener?.deposit?.toString() ?? "?"} USDC base units into stream ${opener?.streamId ?? "?"}; HTTP ${result.response.status}
|
|
473
|
+
`paid ${opener?.deposit?.toString() ?? "?"} USDC base units into stream ${opener?.streamId ?? "?"}; HTTP ${result.response.status}.${split} The seller earns only for VERIFIED seconds; the unearned remainder is NOT refunded automatically — call refund (or refund_status) with this streamId.`,
|
|
393
474
|
structured,
|
|
394
475
|
);
|
|
395
476
|
});
|
|
@@ -405,9 +486,21 @@ export function registerX402Tools(
|
|
|
405
486
|
const bind = async (args: {
|
|
406
487
|
streamId: string;
|
|
407
488
|
refundUrl?: string | undefined;
|
|
408
|
-
}): Promise<
|
|
489
|
+
}): Promise<
|
|
490
|
+
| RefundClient
|
|
491
|
+
| { readonly error: string }
|
|
492
|
+
| { readonly nothingToRefund: string }
|
|
493
|
+
> => {
|
|
409
494
|
const streamId = args.streamId.toLowerCase() as `0x${string}`;
|
|
495
|
+
// Stream id 0 is an upfront-only payment: nothing was escrowed. Never
|
|
496
|
+
// contact a relay or sign anything for it.
|
|
497
|
+
if (streamId === ZERO_STREAM_ID) {
|
|
498
|
+
return { nothingToRefund: streamId };
|
|
499
|
+
}
|
|
410
500
|
const record = await ctx.findStream(streamId);
|
|
501
|
+
if (record?.x402Charge === "upfront-only") {
|
|
502
|
+
return { nothingToRefund: streamId };
|
|
503
|
+
}
|
|
411
504
|
const refundUrl = args.refundUrl ?? record?.refundUrl;
|
|
412
505
|
if (refundUrl === undefined) {
|
|
413
506
|
return {
|
|
@@ -449,9 +542,13 @@ export function registerX402Tools(
|
|
|
449
542
|
try {
|
|
450
543
|
const refunds = await bind(args);
|
|
451
544
|
if ("error" in refunds) return ctx.fail(refunds.error);
|
|
545
|
+
if ("nothingToRefund" in refunds) return nothingToRefund(ctx, args);
|
|
452
546
|
const status = await refunds.status();
|
|
453
547
|
return ctx.ok(describeStatus(status), serializeStatus(status));
|
|
454
548
|
} catch (error) {
|
|
549
|
+
if (error instanceof Error && error.name === "NothingToRefundError") {
|
|
550
|
+
return nothingToRefund(ctx, args);
|
|
551
|
+
}
|
|
455
552
|
return explainRefundError(ctx, error);
|
|
456
553
|
}
|
|
457
554
|
},
|
|
@@ -474,6 +571,7 @@ export function registerX402Tools(
|
|
|
474
571
|
try {
|
|
475
572
|
const refunds = await bind(args);
|
|
476
573
|
if ("error" in refunds) return ctx.fail(refunds.error);
|
|
574
|
+
if ("nothingToRefund" in refunds) return nothingToRefund(ctx, args);
|
|
477
575
|
let closeResult: RefundRelayResult | null = null;
|
|
478
576
|
if (args.close === true) {
|
|
479
577
|
// Buyer recovery, authorized on the mandate owner's signature alone
|
|
@@ -513,12 +611,53 @@ export function registerX402Tools(
|
|
|
513
611
|
},
|
|
514
612
|
);
|
|
515
613
|
} catch (error) {
|
|
614
|
+
if (error instanceof Error && error.name === "NothingToRefundError") {
|
|
615
|
+
return nothingToRefund(ctx, args);
|
|
616
|
+
}
|
|
516
617
|
return explainRefundError(ctx, error);
|
|
517
618
|
}
|
|
518
619
|
},
|
|
519
620
|
);
|
|
520
621
|
}
|
|
521
622
|
|
|
623
|
+
/** Upfront-only payments: a finished, successful answer — not an error. */
|
|
624
|
+
function nothingToRefund(ctx: McpX402Context, args: { streamId: string }) {
|
|
625
|
+
return ctx.ok(
|
|
626
|
+
"nothing to refund: this was an upfront-only payment — the whole price was a non-refundable per-call charge paid to the seller at settlement, no stream was opened and nothing was escrowed. Nothing was requested, signed or sent.",
|
|
627
|
+
{
|
|
628
|
+
streamId: args.streamId.toLowerCase(),
|
|
629
|
+
nothingToRefund: true,
|
|
630
|
+
upfrontOnly: true,
|
|
631
|
+
refundIsAutomatic: false,
|
|
632
|
+
},
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Registry key of a settled upfront-only charge (not an escrow stream id). */
|
|
637
|
+
function chargeKey(
|
|
638
|
+
owner: `0x${string}`,
|
|
639
|
+
opener: `0x${string}`,
|
|
640
|
+
result: PayX402WithMandateResult,
|
|
641
|
+
): `0x${string}` {
|
|
642
|
+
return keccak256(
|
|
643
|
+
toHex(
|
|
644
|
+
`x402-upfront-charge:${owner}:${opener}:${result.authorization?.nonce ?? crypto.randomUUID()}`,
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Read-only chain access for the SDK refund client's opener-kind check. */
|
|
650
|
+
function refundReader(options: McpX402Options): PublicClient {
|
|
651
|
+
return (
|
|
652
|
+
options.sdkOptions?.publicClient ??
|
|
653
|
+
(createPublicClient({
|
|
654
|
+
chain: options.network.chain,
|
|
655
|
+
transport:
|
|
656
|
+
options.sdkOptions?.transport ?? http(options.network.rpcUrls[0]),
|
|
657
|
+
}) as PublicClient)
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
522
661
|
function serializeStatus(status: RefundStatus): Record<string, unknown> {
|
|
523
662
|
return {
|
|
524
663
|
streamId: status.streamId,
|
|
@@ -561,6 +700,28 @@ async function settleFailure(
|
|
|
561
700
|
const post = error as X402PostSignatureError;
|
|
562
701
|
if (post.releasable) {
|
|
563
702
|
await state.release();
|
|
703
|
+
} else if (post.upfrontOnly && post.state === "used") {
|
|
704
|
+
// Settled as an upfront-only charge: no stream to record, but the spend
|
|
705
|
+
// is real and stays counted — as a non-stream charge.
|
|
706
|
+
await ctx.recordStream({
|
|
707
|
+
streamId: keccak256(
|
|
708
|
+
toHex(
|
|
709
|
+
`x402-upfront-charge:${state.owner}:${post.opener}:${post.authorization?.nonce ?? crypto.randomUUID()}`,
|
|
710
|
+
),
|
|
711
|
+
),
|
|
712
|
+
buyer: state.owner,
|
|
713
|
+
operator: post.approved.operator,
|
|
714
|
+
serviceRef: post.quote.serviceRef ?? (`0x${"00".repeat(32)}` as const),
|
|
715
|
+
budgetUsdc: post.approved.budgetUsdc,
|
|
716
|
+
ratePerSecondUsdc: post.approved.ratePerSecondUsdc,
|
|
717
|
+
maxDurationSeconds: post.approved.maxDurationSeconds,
|
|
718
|
+
openedAtSeconds: state.nowSeconds,
|
|
719
|
+
mandateId: state.mandateId,
|
|
720
|
+
opener: post.opener,
|
|
721
|
+
x402Charge: "upfront-only",
|
|
722
|
+
nonRefundableUsdc: post.approved.budgetUsdc,
|
|
723
|
+
});
|
|
724
|
+
await state.release();
|
|
564
725
|
} else if (post.streamId !== null) {
|
|
565
726
|
await ctx.recordStream({
|
|
566
727
|
streamId: post.streamId,
|
|
@@ -573,6 +734,9 @@ async function settleFailure(
|
|
|
573
734
|
openedAtSeconds: state.nowSeconds,
|
|
574
735
|
mandateId: state.mandateId,
|
|
575
736
|
opener: post.opener,
|
|
737
|
+
...(post.quote.upfrontAmount === null || post.quote.upfrontAmount === 0n
|
|
738
|
+
? {}
|
|
739
|
+
: { nonRefundableUsdc: post.quote.upfrontAmount }),
|
|
576
740
|
});
|
|
577
741
|
await state.release();
|
|
578
742
|
}
|
package/src/sdk/client.ts
CHANGED
|
@@ -87,6 +87,11 @@ export interface AgentSdkClient {
|
|
|
87
87
|
claimable(streamId: `0x${string}`): Promise<bigint>;
|
|
88
88
|
reclaimable(streamId: `0x${string}`): Promise<bigint>;
|
|
89
89
|
claim(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
|
|
90
|
+
/**
|
|
91
|
+
* StreamEscrowV3 only: redeem the on-chain proven high-water without a proof.
|
|
92
|
+
* Optional so an existing custom SDK client keeps compiling.
|
|
93
|
+
*/
|
|
94
|
+
claimProven?(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
|
|
90
95
|
close(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
|
|
91
96
|
reclaim(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
|
|
92
97
|
/**
|
|
@@ -192,6 +197,26 @@ export class VerifiedStreamAgentClient implements VerifiedStreamAgentOpener {
|
|
|
192
197
|
return await this.sdkClient.claim(input.streamId);
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Operator-only, StreamEscrowV3 only: redeem the stream's on-chain proven
|
|
202
|
+
* high-water (`claimProven`) without a fresh proof. Authorized exactly like
|
|
203
|
+
* {@link claimStream} — it pays the same party from the same escrow, only an
|
|
204
|
+
* amount already proven against a signed checkpoint root and recorded
|
|
205
|
+
* on-chain, so it can never pay more than a proof claim could have.
|
|
206
|
+
*/
|
|
207
|
+
async claimProvenStream(
|
|
208
|
+
input: MandateAuthorizedStreamActionInput,
|
|
209
|
+
): Promise<StreamProofTransactionResult> {
|
|
210
|
+
const claimProven = this.sdkClient.claimProven?.bind(this.sdkClient);
|
|
211
|
+
if (claimProven === undefined) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"this SDK client cannot send claimProven (StreamEscrowV3 only)",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
await this.authorizeExistingStreamAction(input);
|
|
217
|
+
return await claimProven(input.streamId);
|
|
218
|
+
}
|
|
219
|
+
|
|
195
220
|
async closeStream(
|
|
196
221
|
input: MandateAuthorizedStreamActionInput,
|
|
197
222
|
): Promise<StreamProofTransactionResult> {
|
package/src/x402-buyer/pay.ts
CHANGED
|
@@ -39,6 +39,15 @@ import {
|
|
|
39
39
|
* | `maxDurationSeconds` | `maxDuration` (the stream's window) |
|
|
40
40
|
* | `allowedOperators` | `operator` (who the stream pays) |
|
|
41
41
|
*
|
|
42
|
+
* An UPFRONT ("minimum charge") opener quote is mapped the same way, with
|
|
43
|
+
* `budgetUsdc = price` — the WHOLE payment, of which `upfrontAmount` is
|
|
44
|
+
* non-refundable (paid to the seller at settlement) and only the rest is
|
|
45
|
+
* escrowed. So the per-stream and total caps always count the upfront part as
|
|
46
|
+
* spent, and nothing a refund returns is ever credited back. Its size is also
|
|
47
|
+
* capped separately, before signing, by the SDK's `maxUpfrontAmount` (default
|
|
48
|
+
* 0.10 USDC; pass {@link PayX402WithMandateInput.maxUpfrontAmount} to tighten
|
|
49
|
+
* it, `0n` to refuse every upfront charge).
|
|
50
|
+
*
|
|
42
51
|
* The check runs inside the SDK's `onQuote` hook — after every SDK protection
|
|
43
52
|
* (scheme, network, USDC domain, EIP-3009 only, registered factory, on-chain
|
|
44
53
|
* `predict`, authorization lifetime) and BEFORE anything is signed — so a
|
|
@@ -117,6 +126,12 @@ export interface PayX402WithMandateInput {
|
|
|
117
126
|
readonly maxRatePerSecond?: bigint;
|
|
118
127
|
/** Refuse a quote the seller can fully earn in fewer verified seconds. */
|
|
119
128
|
readonly minDurationSeconds?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Ceiling on the NON-REFUNDABLE upfront part of an upfront-opener quote, base
|
|
131
|
+
* units (the SDK's `maxUpfrontAmount`). Default the SDK's 0.10 USDC; `0n`
|
|
132
|
+
* refuses any upfront charge. Never above the price ceiling in effect.
|
|
133
|
+
*/
|
|
134
|
+
readonly maxUpfrontAmount?: bigint;
|
|
120
135
|
/** Passed through to the SDK (transport, fetch, publicClient, ...). */
|
|
121
136
|
readonly sdkOptions?: Pick<
|
|
122
137
|
PayWithX402Options,
|
|
@@ -179,6 +194,12 @@ export class X402PostSignatureError extends Error {
|
|
|
179
194
|
/** The stream the payment opened, when it could be identified. */
|
|
180
195
|
readonly streamId: `0x${string}` | null;
|
|
181
196
|
readonly opener: `0x${string}`;
|
|
197
|
+
/**
|
|
198
|
+
* `true` when the chain shows (or the signed quote implies) an UPFRONT-ONLY
|
|
199
|
+
* payment: no stream, the whole price was a non-refundable charge, nothing
|
|
200
|
+
* to recover.
|
|
201
|
+
*/
|
|
202
|
+
readonly upfrontOnly: boolean;
|
|
182
203
|
|
|
183
204
|
constructor(context: {
|
|
184
205
|
readonly state: X402PostSignatureState;
|
|
@@ -186,11 +207,16 @@ export class X402PostSignatureError extends Error {
|
|
|
186
207
|
readonly quote: X402QuoteSummary;
|
|
187
208
|
readonly authorization: SignedTransferAuthorization | null;
|
|
188
209
|
readonly streamId: `0x${string}` | null;
|
|
210
|
+
readonly upfrontOnly?: boolean;
|
|
189
211
|
readonly cause: unknown;
|
|
190
212
|
}) {
|
|
213
|
+
const upfrontOnly =
|
|
214
|
+
context.upfrontOnly === true || context.quote.upfrontOnly === true;
|
|
191
215
|
const what =
|
|
192
216
|
context.state === "used"
|
|
193
|
-
?
|
|
217
|
+
? upfrontOnly
|
|
218
|
+
? "the payment SETTLED (the authorization was used) as an UPFRONT-ONLY charge: the whole price went to the seller, no stream was opened and there is nothing to refund"
|
|
219
|
+
: `the payment SETTLED (the authorization was used)${context.streamId === null ? ", but its stream could not be identified" : ` into stream ${context.streamId}`}`
|
|
194
220
|
: context.state === "possibly-used"
|
|
195
221
|
? "the payment MAY HAVE SETTLED: whether the authorization was used could not be established, so treat it as spent"
|
|
196
222
|
: context.state === "unused-but-valid"
|
|
@@ -207,6 +233,7 @@ export class X402PostSignatureError extends Error {
|
|
|
207
233
|
this.authorization = context.authorization;
|
|
208
234
|
this.streamId = context.streamId;
|
|
209
235
|
this.opener = getAddress(context.quote.payTo);
|
|
236
|
+
this.upfrontOnly = upfrontOnly;
|
|
210
237
|
}
|
|
211
238
|
|
|
212
239
|
/** Only an authorization that provably can never move funds frees its reservation. */
|
|
@@ -322,6 +349,8 @@ export async function payX402WithMandate(
|
|
|
322
349
|
mandate.maxRatePerSecondUsdc,
|
|
323
350
|
input.maxRatePerSecond,
|
|
324
351
|
);
|
|
352
|
+
// A negative maxUpfrontAmount is refused by the SDK (InvalidInputError)
|
|
353
|
+
// before the seller is contacted.
|
|
325
354
|
|
|
326
355
|
// 3. The per-quote mandate check, before signing.
|
|
327
356
|
let denial: X402MandateDeniedError | null = null;
|
|
@@ -357,6 +386,9 @@ export async function payX402WithMandate(
|
|
|
357
386
|
network: input.network,
|
|
358
387
|
maxAmount,
|
|
359
388
|
maxRatePerSecond,
|
|
389
|
+
...(input.maxUpfrontAmount === undefined
|
|
390
|
+
? {}
|
|
391
|
+
: { maxUpfrontAmount: smaller(input.maxUpfrontAmount, maxAmount) }),
|
|
360
392
|
...(input.minDurationSeconds === undefined
|
|
361
393
|
? {}
|
|
362
394
|
: { minDurationSeconds: input.minDurationSeconds }),
|
|
@@ -440,6 +472,8 @@ async function classifyPostSignatureFailure(
|
|
|
440
472
|
typeof sdkError.authorizationUsed === "boolean"
|
|
441
473
|
? sdkError.authorizationUsed
|
|
442
474
|
: null;
|
|
475
|
+
let upfrontOnly =
|
|
476
|
+
(sdkError as { readonly upfrontOnly?: boolean }).upfrontOnly === true;
|
|
443
477
|
let streamId: `0x${string}` | null =
|
|
444
478
|
typeof sdkError.streamId === "string" &&
|
|
445
479
|
/^0x[0-9a-fA-F]{64}$/.test(sdkError.streamId)
|
|
@@ -477,16 +511,19 @@ async function classifyPostSignatureFailure(
|
|
|
477
511
|
publicClient: reader as PublicClient,
|
|
478
512
|
});
|
|
479
513
|
streamId = recovery.streamId;
|
|
514
|
+
// An upfront-only payment opens no stream: the opener's
|
|
515
|
+
// UpfrontCharged (streamId 0), read on-chain, proves it was used.
|
|
516
|
+
if (recovery.upfrontOnly) upfrontOnly = true;
|
|
480
517
|
} catch {
|
|
481
518
|
streamId = null;
|
|
482
519
|
}
|
|
483
|
-
if (streamId !== null) used = true;
|
|
520
|
+
if (streamId !== null || upfrontOnly) used = true;
|
|
484
521
|
}
|
|
485
522
|
}
|
|
486
523
|
|
|
487
524
|
const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
|
|
488
525
|
const state: X402PostSignatureState =
|
|
489
|
-
streamId !== null || used === true
|
|
526
|
+
streamId !== null || upfrontOnly || used === true
|
|
490
527
|
? "used"
|
|
491
528
|
: used === false && authorization !== null
|
|
492
529
|
? authorization.validBefore <= nowSeconds
|
|
@@ -499,6 +536,7 @@ async function classifyPostSignatureFailure(
|
|
|
499
536
|
quote: context.quote,
|
|
500
537
|
authorization,
|
|
501
538
|
streamId,
|
|
539
|
+
upfrontOnly,
|
|
502
540
|
cause: context.error,
|
|
503
541
|
});
|
|
504
542
|
}
|
|
@@ -15,7 +15,10 @@ async function main(): Promise<void> {
|
|
|
15
15
|
console.error(
|
|
16
16
|
`[metrik-opener-facilitator] listening on port ${server.boundPort} — chain ${binding.chainId}, ` +
|
|
17
17
|
`factory ${binding.factory}, escrow ${binding.escrow}, token ${binding.usdc}, relayer ${binding.relayer}. ` +
|
|
18
|
-
|
|
18
|
+
(binding.upfront === undefined
|
|
19
|
+
? "Upfront (minimum-charge) opener: OFF. "
|
|
20
|
+
: `Upfront (minimum-charge) opener factory ${binding.upfront.factory} (implementation ${binding.upfront.implementation}): ON — its non-refundable upfrontAmount is paid to the seller at settlement, only price - upfrontAmount is escrowed and refundable. `) +
|
|
21
|
+
"Every payment settles through the opener factory's deployAndSettle (an escrow stream for the metered part); nothing is ever transferred to payTo directly. " +
|
|
19
22
|
(server.refundRelayEnabled
|
|
20
23
|
? "Gasless refund relay ON at /streams/:streamId (close needs the payer's signature; reclaim is permissionless and pays only payerOf). There is no automatic refund: nothing is sent unless an endpoint is called."
|
|
21
24
|
: "Gasless refund relay OFF (METRIK_OPENER_FACILITATOR_REFUND_RELAY=0): a gateway will refuse to quote through this facilitator."),
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "node:http";
|
|
8
8
|
import type { AddressInfo } from "node:net";
|
|
9
9
|
|
|
10
|
-
import { networkForCaip2 } from "@absol-labs/sdk";
|
|
10
|
+
import { networkForCaip2, networkOpenerFactories } from "@absol-labs/sdk";
|
|
11
11
|
import { x402Facilitator } from "@x402/core/facilitator";
|
|
12
12
|
import type { Network } from "@x402/core/types";
|
|
13
13
|
import {
|
|
@@ -470,6 +470,13 @@ export interface OpenerFacilitatorOptions {
|
|
|
470
470
|
readonly rpcUrl: string;
|
|
471
471
|
readonly relayerPrivateKey: Hex;
|
|
472
472
|
readonly factoryAddress: Address;
|
|
473
|
+
/**
|
|
474
|
+
* The `X402UpfrontStreamOpenerFactory` to settle ALSO (same escrow), or
|
|
475
|
+
* undefined for stream openers only. See {@link resolveUpfrontFactory}.
|
|
476
|
+
*/
|
|
477
|
+
readonly upfrontFactoryAddress?: Address;
|
|
478
|
+
/** The implementation the upfront factory must report (the registry's). */
|
|
479
|
+
readonly upfrontImplementation?: Address;
|
|
473
480
|
readonly escrowAddress: Address;
|
|
474
481
|
readonly assetAllowlist: readonly Address[];
|
|
475
482
|
/** Default: a {@link RegistryListingResolver} on the public registry. */
|
|
@@ -528,6 +535,12 @@ export async function buildOpenerFacilitator(
|
|
|
528
535
|
advertiseRefundRelay: options.refundRelay !== undefined,
|
|
529
536
|
network: options.network,
|
|
530
537
|
factoryAddress: options.factoryAddress,
|
|
538
|
+
...(options.upfrontFactoryAddress === undefined
|
|
539
|
+
? {}
|
|
540
|
+
: { upfrontFactoryAddress: options.upfrontFactoryAddress }),
|
|
541
|
+
...(options.upfrontImplementation === undefined
|
|
542
|
+
? {}
|
|
543
|
+
: { upfrontImplementation: options.upfrontImplementation }),
|
|
531
544
|
escrowAddress: options.escrowAddress,
|
|
532
545
|
assetAllowlist: options.assetAllowlist,
|
|
533
546
|
publicClient,
|
|
@@ -553,6 +566,9 @@ export async function buildOpenerFacilitator(
|
|
|
553
566
|
refundRelay = new OpenerRefundRelay({
|
|
554
567
|
network: options.network,
|
|
555
568
|
factoryAddress: options.factoryAddress,
|
|
569
|
+
...(options.upfrontFactoryAddress === undefined
|
|
570
|
+
? {}
|
|
571
|
+
: { upfrontFactoryAddress: options.upfrontFactoryAddress }),
|
|
556
572
|
escrowAddress: options.escrowAddress,
|
|
557
573
|
oracleUrl: relay.oracleUrl,
|
|
558
574
|
publicClient,
|
|
@@ -601,6 +617,10 @@ export interface OpenerFacilitatorEnvConfig {
|
|
|
601
617
|
readonly authToken: string;
|
|
602
618
|
readonly requireAuthOnVerify: boolean;
|
|
603
619
|
readonly factoryAddress: Address;
|
|
620
|
+
/** The upfront opener factory also settled, when enabled. */
|
|
621
|
+
readonly upfrontFactoryAddress?: Address;
|
|
622
|
+
/** Its expected implementation, when the registry records it. */
|
|
623
|
+
readonly upfrontImplementation?: Address;
|
|
604
624
|
readonly escrowAddress: Address;
|
|
605
625
|
readonly assetAllowlist: readonly Address[];
|
|
606
626
|
readonly registryUrl: string;
|
|
@@ -637,6 +657,12 @@ export interface OpenerRefundRelayEnvConfig {
|
|
|
637
657
|
* registry entry; on a network the registry does not know (or one with no
|
|
638
658
|
* opener factory) each must be given explicitly.
|
|
639
659
|
* - `METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS` — default the network's registered factory.
|
|
660
|
+
* - `METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS` — the
|
|
661
|
+
* `X402UpfrontStreamOpenerFactory` (upfront "minimum charge" listings) settled
|
|
662
|
+
* ALONGSIDE the stream factory. Default: the registry's upfront factory for
|
|
663
|
+
* the configured escrow, when it records one (Base Sepolia: bound to
|
|
664
|
+
* StreamEscrowV3); none otherwise. `off` disables it. It must be bound to the
|
|
665
|
+
* same escrow and token, or the facilitator refuses to start.
|
|
640
666
|
* - `METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS` — default the network's registered `StreamEscrowV2`.
|
|
641
667
|
* - `METRIK_OPENER_FACILITATOR_ASSETS` — comma-separated allowlist; default the network's USDC.
|
|
642
668
|
* - `METRIK_OPENER_FACILITATOR_REGISTRY_URL` — default the public listings registry.
|
|
@@ -736,6 +762,11 @@ export function parseOpenerFacilitatorEnvConfig(
|
|
|
736
762
|
"METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS",
|
|
737
763
|
);
|
|
738
764
|
}
|
|
765
|
+
const upfront = resolveUpfrontFactory(
|
|
766
|
+
env.METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS,
|
|
767
|
+
network,
|
|
768
|
+
escrowAddress,
|
|
769
|
+
);
|
|
739
770
|
const assetsRaw = env.METRIK_OPENER_FACILITATOR_ASSETS?.trim();
|
|
740
771
|
let assetAllowlist: Address[];
|
|
741
772
|
if (assetsRaw === undefined || assetsRaw.length === 0) {
|
|
@@ -805,6 +836,7 @@ export function parseOpenerFacilitatorEnvConfig(
|
|
|
805
836
|
DEFAULT_SETTLE_RATE_LIMIT_PER_MINUTE,
|
|
806
837
|
),
|
|
807
838
|
factoryAddress,
|
|
839
|
+
...(upfront === undefined ? {} : upfront),
|
|
808
840
|
escrowAddress,
|
|
809
841
|
assetAllowlist,
|
|
810
842
|
registryUrl,
|
|
@@ -813,6 +845,68 @@ export function parseOpenerFacilitatorEnvConfig(
|
|
|
813
845
|
};
|
|
814
846
|
}
|
|
815
847
|
|
|
848
|
+
/**
|
|
849
|
+
* The upfront opener factory the facilitator also settles, from
|
|
850
|
+
* `METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS`: unset = the network
|
|
851
|
+
* registry's upfront factory OF THE CONFIGURED ESCROW'S deployment (so a
|
|
852
|
+
* facilitator pinned to a legacy escrow never picks up a factory bound to
|
|
853
|
+
* another one); `off`/`none`/`0`/`false` = disabled; an address = that factory
|
|
854
|
+
* (its implementation is pinned only when the registry records it).
|
|
855
|
+
*/
|
|
856
|
+
export function resolveUpfrontFactory(
|
|
857
|
+
raw: string | undefined,
|
|
858
|
+
network: Network,
|
|
859
|
+
escrowAddress: Address,
|
|
860
|
+
):
|
|
861
|
+
| {
|
|
862
|
+
readonly upfrontFactoryAddress: Address;
|
|
863
|
+
readonly upfrontImplementation?: Address;
|
|
864
|
+
}
|
|
865
|
+
| undefined {
|
|
866
|
+
const value = raw?.trim() ?? "";
|
|
867
|
+
const registered = networkForCaip2(network);
|
|
868
|
+
const forEscrow =
|
|
869
|
+
registered === undefined
|
|
870
|
+
? undefined
|
|
871
|
+
: networkOpenerFactories(registered).find(
|
|
872
|
+
(entry) =>
|
|
873
|
+
entry.kind === "upfront" &&
|
|
874
|
+
getAddress(entry.deployment.escrow.address) ===
|
|
875
|
+
getAddress(escrowAddress),
|
|
876
|
+
);
|
|
877
|
+
if (value.length === 0) {
|
|
878
|
+
if (forEscrow === undefined) return undefined;
|
|
879
|
+
return {
|
|
880
|
+
upfrontFactoryAddress: getAddress(forEscrow.address),
|
|
881
|
+
...(forEscrow.implementation === null
|
|
882
|
+
? {}
|
|
883
|
+
: { upfrontImplementation: getAddress(forEscrow.implementation) }),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (["off", "none", "0", "false", "no"].includes(value.toLowerCase())) {
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
889
|
+
if (!isAddress(value)) {
|
|
890
|
+
throw new Error(
|
|
891
|
+
"METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS must be an EVM address (or off)",
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
const known =
|
|
895
|
+
registered === undefined
|
|
896
|
+
? undefined
|
|
897
|
+
: networkOpenerFactories(registered).find(
|
|
898
|
+
(entry) =>
|
|
899
|
+
entry.kind === "upfront" &&
|
|
900
|
+
getAddress(entry.address) === getAddress(value),
|
|
901
|
+
);
|
|
902
|
+
return {
|
|
903
|
+
upfrontFactoryAddress: getAddress(value),
|
|
904
|
+
...(known?.implementation == null
|
|
905
|
+
? {}
|
|
906
|
+
: { upfrontImplementation: getAddress(known.implementation) }),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
816
910
|
function parseRefundRelayEnv(
|
|
817
911
|
env: NodeJS.ProcessEnv,
|
|
818
912
|
network: Network,
|
|
@@ -914,6 +1008,12 @@ export async function startOpenerFacilitatorServerFromEnv(
|
|
|
914
1008
|
rpcUrl: config.rpcUrl,
|
|
915
1009
|
relayerPrivateKey: config.relayerPrivateKey,
|
|
916
1010
|
factoryAddress: config.factoryAddress,
|
|
1011
|
+
...(config.upfrontFactoryAddress === undefined
|
|
1012
|
+
? {}
|
|
1013
|
+
: { upfrontFactoryAddress: config.upfrontFactoryAddress }),
|
|
1014
|
+
...(config.upfrontImplementation === undefined
|
|
1015
|
+
? {}
|
|
1016
|
+
: { upfrontImplementation: config.upfrontImplementation }),
|
|
917
1017
|
escrowAddress: config.escrowAddress,
|
|
918
1018
|
assetAllowlist: config.assetAllowlist,
|
|
919
1019
|
registryUrl: config.registryUrl,
|