@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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
streamEscrowV2Abi,
|
|
3
|
+
x402UpfrontStreamOpenerAbi,
|
|
4
|
+
x402UpfrontStreamOpenerFactoryAbi,
|
|
5
|
+
} from "@absol-labs/shared";
|
|
2
6
|
import type {
|
|
3
7
|
FacilitatorContext,
|
|
4
8
|
Network,
|
|
@@ -39,15 +43,30 @@ import type { OperatorSignedListingResolver } from "./listing-resolver.js";
|
|
|
39
43
|
import { RelayerQueue } from "./relayer-queue.js";
|
|
40
44
|
import {
|
|
41
45
|
chainIdFromNetwork,
|
|
46
|
+
assertValidUpfrontTerms,
|
|
42
47
|
maxEarnable,
|
|
43
48
|
OPENER_EXTRA_KEY,
|
|
44
49
|
OPENER_SCHEME,
|
|
50
|
+
OPENER_TERMS_VERSION,
|
|
45
51
|
parseUintString,
|
|
46
52
|
termsFromExtra,
|
|
47
53
|
termsTuple,
|
|
54
|
+
upfrontRefundable,
|
|
55
|
+
upfrontTermsTuple,
|
|
56
|
+
type OpenerKind,
|
|
48
57
|
type OpenerTerms,
|
|
58
|
+
type UpfrontOpenerTerms,
|
|
49
59
|
} from "./terms.js";
|
|
50
60
|
|
|
61
|
+
/**
|
|
62
|
+
* What one checked payment settles: the factory's kind and its terms. An
|
|
63
|
+
* `"upfront"` payment carries the 6-field terms (price + non-refundable
|
|
64
|
+
* upfrontAmount); a `"stream"` payment the 4-field ones.
|
|
65
|
+
*/
|
|
66
|
+
export type OpenerSettlementPlan =
|
|
67
|
+
| { readonly kind: "stream"; readonly terms: OpenerTerms }
|
|
68
|
+
| { readonly kind: "upfront"; readonly terms: UpfrontOpenerTerms };
|
|
69
|
+
|
|
51
70
|
/**
|
|
52
71
|
* `MetrikOpenerExactScheme` — an x402 v2 `exact` facilitator scheme that
|
|
53
72
|
* settles every payment INTO a `StreamEscrowV2` stream through the ownerless
|
|
@@ -104,6 +123,20 @@ import {
|
|
|
104
123
|
*
|
|
105
124
|
* Relayer submissions (simulate + write) are serialized so concurrent payers
|
|
106
125
|
* never race for the relayer's account nonce.
|
|
126
|
+
*
|
|
127
|
+
* ## The upfront ("minimum charge") opener kind
|
|
128
|
+
*
|
|
129
|
+
* With `upfrontFactoryAddress` set, a quote may also name the
|
|
130
|
+
* `X402UpfrontStreamOpenerFactory` (metrik-contracts#54). Its 6-field terms
|
|
131
|
+
* add `price` and a non-refundable `upfrontAmount`; the kind is decided by
|
|
132
|
+
* WHICH configured factory the quote names, never by the wire fields. Verify
|
|
133
|
+
* additionally requires the factory's own `validateTerms` rules,
|
|
134
|
+
* `price - upfrontAmount <= rate * maxDuration`, `amount == price` and
|
|
135
|
+
* `payTo == upfrontFactory.predict(terms)`, and simulates the 6-tuple
|
|
136
|
+
* `deployAndSettle`. Settle succeeds only on a receipt that shows exactly the
|
|
137
|
+
* signed split (see {@link MetrikOpenerExactScheme.upfrontSettlement}); an
|
|
138
|
+
* upfront-only payment (`upfrontAmount == price`) opens no stream and is
|
|
139
|
+
* reported with `streamId: null`.
|
|
107
140
|
*/
|
|
108
141
|
|
|
109
142
|
/** Reasons this scheme adds to the stock `exact` ones. All fail closed. */
|
|
@@ -156,6 +189,19 @@ export interface MetrikOpenerExactSchemeConfig {
|
|
|
156
189
|
readonly network: Network;
|
|
157
190
|
/** The deployed `X402StreamOpenerFactory`. */
|
|
158
191
|
readonly factoryAddress: Address;
|
|
192
|
+
/**
|
|
193
|
+
* The deployed `X402UpfrontStreamOpenerFactory` (metrik-contracts#54), bound
|
|
194
|
+
* to the SAME escrow. When set, upfront ("minimum charge") payments are
|
|
195
|
+
* accepted too and `/supported` advertises it as `upfrontOpener`. Unset = the
|
|
196
|
+
* stream factory only (the pre-upfront behaviour).
|
|
197
|
+
*/
|
|
198
|
+
readonly upfrontFactoryAddress?: Address;
|
|
199
|
+
/**
|
|
200
|
+
* The implementation `upfrontFactoryAddress` must report
|
|
201
|
+
* (`factory.implementation()`), e.g. the registry's. Checked by
|
|
202
|
+
* {@link MetrikOpenerExactScheme.init} when given.
|
|
203
|
+
*/
|
|
204
|
+
readonly upfrontImplementation?: Address;
|
|
159
205
|
/** The `StreamEscrowV2` the factory must point at (checked by {@link init}). */
|
|
160
206
|
readonly escrowAddress: Address;
|
|
161
207
|
/** Settlement tokens this facilitator accepts. Must contain `factory.usdc()`. */
|
|
@@ -211,7 +257,7 @@ type LedgerEntry =
|
|
|
211
257
|
readonly payload: PaymentPayload;
|
|
212
258
|
readonly requirements: PaymentRequirements;
|
|
213
259
|
readonly auth: ExactEIP3009Payload & { signature: Hex };
|
|
214
|
-
readonly
|
|
260
|
+
readonly plan: OpenerSettlementPlan;
|
|
215
261
|
readonly opener: Address;
|
|
216
262
|
}
|
|
217
263
|
| {
|
|
@@ -227,6 +273,19 @@ export interface OpenerFacilitatorBinding {
|
|
|
227
273
|
readonly escrow: Address;
|
|
228
274
|
readonly usdc: Address;
|
|
229
275
|
readonly relayer: Address;
|
|
276
|
+
/** The upfront factory, when configured (same escrow and token). */
|
|
277
|
+
readonly upfront?: {
|
|
278
|
+
readonly factory: Address;
|
|
279
|
+
readonly implementation: Address;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The upfront opener kind advertised in the `/supported` marker. */
|
|
284
|
+
export interface UpfrontOpenerMarker {
|
|
285
|
+
readonly factory: Address;
|
|
286
|
+
readonly implementation: Address;
|
|
287
|
+
readonly escrow: Address;
|
|
288
|
+
readonly termsVersion: typeof OPENER_TERMS_VERSION.upfront;
|
|
230
289
|
}
|
|
231
290
|
|
|
232
291
|
/**
|
|
@@ -251,6 +310,12 @@ export interface OpenerFacilitatorMarker {
|
|
|
251
310
|
* Absent (read as false) otherwise.
|
|
252
311
|
*/
|
|
253
312
|
readonly refundRelay?: true;
|
|
313
|
+
/**
|
|
314
|
+
* Present when this facilitator also settles the UPFRONT ("minimum charge")
|
|
315
|
+
* opener kind. `factory` above stays the stream factory, so a gateway that
|
|
316
|
+
* predates the upfront kind reads the marker exactly as before.
|
|
317
|
+
*/
|
|
318
|
+
readonly upfrontOpener?: UpfrontOpenerMarker;
|
|
254
319
|
}
|
|
255
320
|
|
|
256
321
|
export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
@@ -276,6 +341,22 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
276
341
|
if (!isAddress(config.escrowAddress)) {
|
|
277
342
|
throw new Error("escrowAddress must be an EVM address");
|
|
278
343
|
}
|
|
344
|
+
if (
|
|
345
|
+
config.upfrontFactoryAddress !== undefined &&
|
|
346
|
+
(!isAddress(config.upfrontFactoryAddress) ||
|
|
347
|
+
getAddress(config.upfrontFactoryAddress) ===
|
|
348
|
+
getAddress(config.factoryAddress))
|
|
349
|
+
) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
"upfrontFactoryAddress must be an EVM address other than factoryAddress",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (
|
|
355
|
+
config.upfrontImplementation !== undefined &&
|
|
356
|
+
!isAddress(config.upfrontImplementation)
|
|
357
|
+
) {
|
|
358
|
+
throw new Error("upfrontImplementation must be an EVM address");
|
|
359
|
+
}
|
|
279
360
|
if (config.assetAllowlist.length === 0) {
|
|
280
361
|
throw new Error("assetAllowlist must not be empty");
|
|
281
362
|
}
|
|
@@ -372,12 +453,65 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
372
453
|
`factory settlement token ${factoryUsdc} is not on the asset allowlist`,
|
|
373
454
|
);
|
|
374
455
|
}
|
|
456
|
+
let upfront: OpenerFacilitatorBinding["upfront"];
|
|
457
|
+
if (this.config.upfrontFactoryAddress !== undefined) {
|
|
458
|
+
const upfrontFactory = getAddress(this.config.upfrontFactoryAddress);
|
|
459
|
+
const upfrontCode = await publicClient.getCode({
|
|
460
|
+
address: upfrontFactory,
|
|
461
|
+
});
|
|
462
|
+
if (upfrontCode === undefined || upfrontCode === "0x") {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`no contract at upfront factory address ${upfrontFactory}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const [upEscrow, upUsdc, upImplementation] = await Promise.all([
|
|
468
|
+
publicClient.readContract({
|
|
469
|
+
address: upfrontFactory,
|
|
470
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
471
|
+
functionName: "escrow",
|
|
472
|
+
}),
|
|
473
|
+
publicClient.readContract({
|
|
474
|
+
address: upfrontFactory,
|
|
475
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
476
|
+
functionName: "usdc",
|
|
477
|
+
}),
|
|
478
|
+
publicClient.readContract({
|
|
479
|
+
address: upfrontFactory,
|
|
480
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
481
|
+
functionName: "implementation",
|
|
482
|
+
}),
|
|
483
|
+
]);
|
|
484
|
+
if (getAddress(upEscrow) !== getAddress(escrowAddress)) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`upfront factory escrow ${upEscrow} is not the configured escrow ${escrowAddress}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (getAddress(upUsdc) !== getAddress(factoryUsdc)) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`upfront factory token ${upUsdc} is not the stream factory's ${factoryUsdc}`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (
|
|
495
|
+
this.config.upfrontImplementation !== undefined &&
|
|
496
|
+
getAddress(upImplementation) !==
|
|
497
|
+
getAddress(this.config.upfrontImplementation)
|
|
498
|
+
) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`upfront factory implementation ${upImplementation} is not the expected ${this.config.upfrontImplementation}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
upfront = {
|
|
504
|
+
factory: upfrontFactory,
|
|
505
|
+
implementation: getAddress(upImplementation),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
375
508
|
this.binding = {
|
|
376
509
|
chainId: this.chainId,
|
|
377
510
|
factory: getAddress(factoryAddress),
|
|
378
511
|
escrow: getAddress(factoryEscrow),
|
|
379
512
|
usdc: getAddress(factoryUsdc),
|
|
380
513
|
relayer: this.config.walletClient.account.address,
|
|
514
|
+
...(upfront === undefined ? {} : { upfront }),
|
|
381
515
|
};
|
|
382
516
|
return this.binding;
|
|
383
517
|
}
|
|
@@ -399,6 +533,16 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
399
533
|
...(this.config.advertiseRefundRelay === true
|
|
400
534
|
? { refundRelay: true as const }
|
|
401
535
|
: {}),
|
|
536
|
+
...(binding.upfront === undefined
|
|
537
|
+
? {}
|
|
538
|
+
: {
|
|
539
|
+
upfrontOpener: {
|
|
540
|
+
factory: binding.upfront.factory,
|
|
541
|
+
implementation: binding.upfront.implementation,
|
|
542
|
+
escrow: binding.escrow,
|
|
543
|
+
termsVersion: OPENER_TERMS_VERSION.upfront,
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
402
546
|
};
|
|
403
547
|
return { [OPENER_EXTRA_KEY]: marker };
|
|
404
548
|
}
|
|
@@ -500,7 +644,7 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
500
644
|
await this.awaitSettlement(
|
|
501
645
|
existing.hash,
|
|
502
646
|
existing.auth,
|
|
503
|
-
existing.
|
|
647
|
+
existing.plan,
|
|
504
648
|
existing.opener,
|
|
505
649
|
network,
|
|
506
650
|
),
|
|
@@ -536,10 +680,10 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
536
680
|
return refusal(checked.response, network, payer);
|
|
537
681
|
}
|
|
538
682
|
|
|
539
|
-
const { auth,
|
|
683
|
+
const { auth, plan, opener } = checked;
|
|
540
684
|
let hash: Hex;
|
|
541
685
|
try {
|
|
542
|
-
hash = await this.submitSerialized(auth,
|
|
686
|
+
hash = await this.submitSerialized(auth, plan);
|
|
543
687
|
} catch (error) {
|
|
544
688
|
this.ledger.delete(key);
|
|
545
689
|
return failure(
|
|
@@ -555,8 +699,8 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
555
699
|
payload,
|
|
556
700
|
requirements,
|
|
557
701
|
validBefore,
|
|
558
|
-
await this.awaitSettlement(hash, auth,
|
|
559
|
-
{ auth,
|
|
702
|
+
await this.awaitSettlement(hash, auth, plan, opener, network),
|
|
703
|
+
{ auth, plan, opener },
|
|
560
704
|
);
|
|
561
705
|
} catch (error) {
|
|
562
706
|
// Unknown state after a possible broadcast: keep the authorization
|
|
@@ -585,7 +729,7 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
585
729
|
result: SettleResponse,
|
|
586
730
|
context: {
|
|
587
731
|
readonly auth: ExactEIP3009Payload & { signature: Hex };
|
|
588
|
-
readonly
|
|
732
|
+
readonly plan: OpenerSettlementPlan;
|
|
589
733
|
readonly opener: Address;
|
|
590
734
|
},
|
|
591
735
|
): SettleResponse {
|
|
@@ -606,7 +750,7 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
606
750
|
payload,
|
|
607
751
|
requirements,
|
|
608
752
|
auth: context.auth,
|
|
609
|
-
|
|
753
|
+
plan: context.plan,
|
|
610
754
|
opener: context.opener,
|
|
611
755
|
});
|
|
612
756
|
return result;
|
|
@@ -633,16 +777,10 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
633
777
|
*/
|
|
634
778
|
private submitSerialized(
|
|
635
779
|
auth: ExactEIP3009Payload & { signature: Hex },
|
|
636
|
-
|
|
780
|
+
plan: OpenerSettlementPlan,
|
|
637
781
|
): Promise<Hex> {
|
|
638
782
|
const run = async (): Promise<Hex> => {
|
|
639
|
-
const { request } = await this.
|
|
640
|
-
account: this.config.walletClient.account,
|
|
641
|
-
address: this.config.factoryAddress,
|
|
642
|
-
abi: x402StreamOpenerFactoryAbi,
|
|
643
|
-
functionName: "deployAndSettle",
|
|
644
|
-
args: [termsTuple(terms), authorizationTuple(auth)],
|
|
645
|
-
});
|
|
783
|
+
const { request } = await this.simulateDeployAndSettle(auth, plan);
|
|
646
784
|
return this.config.walletClient.writeContract({
|
|
647
785
|
...request,
|
|
648
786
|
chain: this.config.walletClient.chain ?? null,
|
|
@@ -680,10 +818,41 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
680
818
|
}
|
|
681
819
|
}
|
|
682
820
|
|
|
821
|
+
/**
|
|
822
|
+
* `eth_call` (simulate) `deployAndSettle` on the factory of the plan's kind:
|
|
823
|
+
* the 4-tuple stream factory, or the 6-tuple upfront factory.
|
|
824
|
+
*/
|
|
825
|
+
private async simulateDeployAndSettle(
|
|
826
|
+
auth: ExactEIP3009Payload & { signature: Hex },
|
|
827
|
+
plan: OpenerSettlementPlan,
|
|
828
|
+
) {
|
|
829
|
+
const { publicClient, walletClient } = this.config;
|
|
830
|
+
if (plan.kind === "upfront") {
|
|
831
|
+
const factory = this.binding?.upfront?.factory;
|
|
832
|
+
if (factory === undefined) {
|
|
833
|
+
throw new Error("the upfront opener factory is not configured");
|
|
834
|
+
}
|
|
835
|
+
return await publicClient.simulateContract({
|
|
836
|
+
account: walletClient.account,
|
|
837
|
+
address: factory,
|
|
838
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
839
|
+
functionName: "deployAndSettle",
|
|
840
|
+
args: [upfrontTermsTuple(plan.terms), authorizationTuple(auth)],
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return await publicClient.simulateContract({
|
|
844
|
+
account: walletClient.account,
|
|
845
|
+
address: this.config.factoryAddress,
|
|
846
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
847
|
+
functionName: "deployAndSettle",
|
|
848
|
+
args: [termsTuple(plan.terms), authorizationTuple(auth)],
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
683
852
|
private async awaitSettlement(
|
|
684
853
|
hash: Hex,
|
|
685
854
|
auth: ExactEIP3009Payload & { signature: Hex },
|
|
686
|
-
|
|
855
|
+
plan: OpenerSettlementPlan,
|
|
687
856
|
opener: Address,
|
|
688
857
|
network: Network,
|
|
689
858
|
): Promise<SettleResponse> {
|
|
@@ -714,12 +883,24 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
714
883
|
hash,
|
|
715
884
|
);
|
|
716
885
|
}
|
|
886
|
+
if (plan.kind === "upfront") {
|
|
887
|
+
return this.upfrontSettlement(
|
|
888
|
+
receipt.logs,
|
|
889
|
+
hash,
|
|
890
|
+
auth,
|
|
891
|
+
plan.terms,
|
|
892
|
+
opener,
|
|
893
|
+
network,
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const terms = plan.terms;
|
|
717
897
|
const opened = findStreamOpened(
|
|
718
898
|
receipt.logs,
|
|
719
899
|
opener,
|
|
720
900
|
getAddress(this.config.escrowAddress),
|
|
721
901
|
auth,
|
|
722
902
|
terms,
|
|
903
|
+
BigInt(auth.authorization.value),
|
|
723
904
|
);
|
|
724
905
|
if (opened === undefined) {
|
|
725
906
|
return failure(
|
|
@@ -750,6 +931,104 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
750
931
|
};
|
|
751
932
|
}
|
|
752
933
|
|
|
934
|
+
/**
|
|
935
|
+
* The success response of a mined UPFRONT settlement, or a mismatch failure.
|
|
936
|
+
* The receipt must show exactly what the signed terms imply:
|
|
937
|
+
*
|
|
938
|
+
* - `upfrontAmount > 0`: the opener's `UpfrontCharged` for this payer, nonce
|
|
939
|
+
* and operator, for exactly `upfrontAmount`, naming the stream it opened
|
|
940
|
+
* (or `bytes32(0)` when none was);
|
|
941
|
+
* - `price > upfrontAmount`: the escrow's `StreamOpened` (buyer = opener) and
|
|
942
|
+
* the opener's `StreamOpenedFor` with `deposit = price - upfrontAmount`;
|
|
943
|
+
* - `upfrontAmount == price` (upfront-only): NO stream at all. Success, with
|
|
944
|
+
* `streamId: null`: nothing was escrowed and there is nothing to refund.
|
|
945
|
+
*
|
|
946
|
+
* The reported `upfrontAmount` / `upfrontFee` are read from `UpfrontCharged`.
|
|
947
|
+
*/
|
|
948
|
+
/** @internal */
|
|
949
|
+
private upfrontSettlement(
|
|
950
|
+
logs: readonly Log[],
|
|
951
|
+
hash: Hex,
|
|
952
|
+
auth: ExactEIP3009Payload & { signature: Hex },
|
|
953
|
+
terms: UpfrontOpenerTerms,
|
|
954
|
+
opener: Address,
|
|
955
|
+
network: Network,
|
|
956
|
+
): SettleResponse {
|
|
957
|
+
const payer = getAddress(auth.authorization.from);
|
|
958
|
+
const escrow = getAddress(this.config.escrowAddress);
|
|
959
|
+
const refundable = upfrontRefundable(terms);
|
|
960
|
+
const mismatch = (why: string) =>
|
|
961
|
+
failure(
|
|
962
|
+
OpenerInvalidReason.streamEventMismatch,
|
|
963
|
+
network,
|
|
964
|
+
payer,
|
|
965
|
+
`the mined receipt does not show the expected upfront settlement: ${why}`,
|
|
966
|
+
hash,
|
|
967
|
+
);
|
|
968
|
+
const charged = findUpfrontCharged(logs, opener, auth, terms);
|
|
969
|
+
if (terms.upfrontAmount > 0n && charged === undefined) {
|
|
970
|
+
return mismatch(`no UpfrontCharged for ${terms.upfrontAmount}`);
|
|
971
|
+
}
|
|
972
|
+
if (terms.upfrontAmount === 0n && charged !== undefined) {
|
|
973
|
+
return mismatch("an UpfrontCharged for terms without an upfront part");
|
|
974
|
+
}
|
|
975
|
+
let streamId: Hex | null = null;
|
|
976
|
+
let expiresAt: bigint | null = null;
|
|
977
|
+
if (refundable > 0n) {
|
|
978
|
+
const opened = findStreamOpened(
|
|
979
|
+
logs,
|
|
980
|
+
opener,
|
|
981
|
+
escrow,
|
|
982
|
+
auth,
|
|
983
|
+
terms,
|
|
984
|
+
refundable,
|
|
985
|
+
);
|
|
986
|
+
if (opened === undefined) {
|
|
987
|
+
return mismatch(`no stream with deposit ${refundable}`);
|
|
988
|
+
}
|
|
989
|
+
if (
|
|
990
|
+
charged !== undefined &&
|
|
991
|
+
charged.streamId.toLowerCase() !== opened.streamId.toLowerCase()
|
|
992
|
+
) {
|
|
993
|
+
return mismatch("UpfrontCharged names another stream");
|
|
994
|
+
}
|
|
995
|
+
streamId = opened.streamId;
|
|
996
|
+
expiresAt = opened.expiresAt;
|
|
997
|
+
} else {
|
|
998
|
+
// Upfront-only: the opener must have opened nothing.
|
|
999
|
+
if (charged === undefined || !/^0x0{64}$/i.test(charged.streamId)) {
|
|
1000
|
+
return mismatch("an upfront-only payment must report streamId 0");
|
|
1001
|
+
}
|
|
1002
|
+
if (
|
|
1003
|
+
findStreamOpened(logs, opener, escrow, auth, terms, undefined) !==
|
|
1004
|
+
undefined
|
|
1005
|
+
) {
|
|
1006
|
+
return mismatch("an upfront-only payment opened a stream");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
success: true,
|
|
1011
|
+
transaction: hash,
|
|
1012
|
+
network,
|
|
1013
|
+
payer,
|
|
1014
|
+
amount: auth.authorization.value,
|
|
1015
|
+
extra: {
|
|
1016
|
+
[OPENER_EXTRA_KEY]: {
|
|
1017
|
+
// `null` (not bytes32(0)) for an upfront-only payment: no stream.
|
|
1018
|
+
streamId,
|
|
1019
|
+
opener,
|
|
1020
|
+
escrow,
|
|
1021
|
+
// The refundable, metered part — the most any refund can return.
|
|
1022
|
+
deposit: refundable.toString(),
|
|
1023
|
+
ratePerSecond: terms.ratePerSecond.toString(),
|
|
1024
|
+
...(expiresAt === null ? {} : { expiresAt: Number(expiresAt) }),
|
|
1025
|
+
upfrontAmount: (charged?.amount ?? 0n).toString(),
|
|
1026
|
+
upfrontFee: (charged?.fee ?? 0n).toString(),
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
753
1032
|
/** Every Metrik check plus the stock verifier. Cheapest, most local first. */
|
|
754
1033
|
private async check(
|
|
755
1034
|
payload: PaymentPayload,
|
|
@@ -759,7 +1038,7 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
759
1038
|
| {
|
|
760
1039
|
readonly ok: true;
|
|
761
1040
|
readonly auth: ExactEIP3009Payload & { signature: Hex };
|
|
762
|
-
readonly
|
|
1041
|
+
readonly plan: OpenerSettlementPlan;
|
|
763
1042
|
readonly opener: Address;
|
|
764
1043
|
}
|
|
765
1044
|
| { readonly ok: false; readonly response: VerifyResponse }
|
|
@@ -816,16 +1095,59 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
816
1095
|
return refuse(OpenerInvalidReason.assetNotAllowed);
|
|
817
1096
|
}
|
|
818
1097
|
|
|
819
|
-
let
|
|
820
|
-
let factory: Address;
|
|
1098
|
+
let parsed: ReturnType<typeof termsFromExtra>;
|
|
821
1099
|
try {
|
|
822
|
-
|
|
1100
|
+
parsed = termsFromExtra(requirements.extra);
|
|
823
1101
|
} catch (error) {
|
|
824
1102
|
return refuse(OpenerInvalidReason.invalidTerms, describe(error));
|
|
825
1103
|
}
|
|
826
|
-
|
|
1104
|
+
const { terms, factory } = parsed;
|
|
1105
|
+
// Only a factory this facilitator is bound to (and proved in init) is
|
|
1106
|
+
// ever settled; its kind decides the terms shape — never the wire fields.
|
|
1107
|
+
let kind: OpenerKind;
|
|
1108
|
+
if (factory === binding.factory) {
|
|
1109
|
+
kind = "stream";
|
|
1110
|
+
} else if (
|
|
1111
|
+
binding.upfront !== undefined &&
|
|
1112
|
+
factory === binding.upfront.factory
|
|
1113
|
+
) {
|
|
1114
|
+
kind = "upfront";
|
|
1115
|
+
} else {
|
|
827
1116
|
return refuse(OpenerInvalidReason.factoryMismatch);
|
|
828
1117
|
}
|
|
1118
|
+
if (
|
|
1119
|
+
parsed.termsVersion !== undefined &&
|
|
1120
|
+
parsed.termsVersion !== OPENER_TERMS_VERSION[kind]
|
|
1121
|
+
) {
|
|
1122
|
+
return refuse(
|
|
1123
|
+
OpenerInvalidReason.invalidTerms,
|
|
1124
|
+
`termsVersion ${parsed.termsVersion} does not match the ${kind} opener factory (${OPENER_TERMS_VERSION[kind]})`,
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
let plan: OpenerSettlementPlan;
|
|
1128
|
+
if (kind === "upfront") {
|
|
1129
|
+
if (parsed.upfront === undefined) {
|
|
1130
|
+
return refuse(
|
|
1131
|
+
OpenerInvalidReason.invalidTerms,
|
|
1132
|
+
"the upfront opener factory needs price and upfrontAmount in the terms",
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const upfrontTerms: UpfrontOpenerTerms = { ...terms, ...parsed.upfront };
|
|
1136
|
+
try {
|
|
1137
|
+
assertValidUpfrontTerms(upfrontTerms);
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
return refuse(OpenerInvalidReason.invalidTerms, describe(error));
|
|
1140
|
+
}
|
|
1141
|
+
plan = { kind, terms: upfrontTerms };
|
|
1142
|
+
} else {
|
|
1143
|
+
if (parsed.upfront !== undefined) {
|
|
1144
|
+
return refuse(
|
|
1145
|
+
OpenerInvalidReason.invalidTerms,
|
|
1146
|
+
"price/upfrontAmount are not terms of the stream opener factory (its payTo does not commit to them)",
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
plan = { kind, terms };
|
|
1150
|
+
}
|
|
829
1151
|
|
|
830
1152
|
let amount: bigint;
|
|
831
1153
|
try {
|
|
@@ -842,16 +1164,41 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
842
1164
|
`amount ${amount} is below this facilitator's minimum ${this.minAmount}`,
|
|
843
1165
|
);
|
|
844
1166
|
}
|
|
845
|
-
if (
|
|
1167
|
+
if (plan.kind === "upfront") {
|
|
1168
|
+
// The opener accepts exactly its price; anything else reverts on-chain.
|
|
1169
|
+
if (amount !== plan.terms.price) {
|
|
1170
|
+
return refuse(
|
|
1171
|
+
OpenerInvalidReason.invalidAmount,
|
|
1172
|
+
`amount ${amount} is not the upfront opener's fixed price ${plan.terms.price}`,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
} else if (amount > maxEarnable(terms)) {
|
|
846
1176
|
return refuse(OpenerInvalidReason.amountExceedsTerms);
|
|
847
1177
|
}
|
|
848
1178
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1179
|
+
let predicted: Address;
|
|
1180
|
+
try {
|
|
1181
|
+
predicted =
|
|
1182
|
+
plan.kind === "upfront"
|
|
1183
|
+
? await this.config.publicClient.readContract({
|
|
1184
|
+
address: binding.upfront?.factory as Address,
|
|
1185
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
1186
|
+
functionName: "predict",
|
|
1187
|
+
args: [upfrontTermsTuple(plan.terms)],
|
|
1188
|
+
})
|
|
1189
|
+
: await this.config.publicClient.readContract({
|
|
1190
|
+
address: binding.factory,
|
|
1191
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
1192
|
+
functionName: "predict",
|
|
1193
|
+
args: [termsTuple(terms)],
|
|
1194
|
+
});
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
// predict reverts InvalidTerms for terms no transaction can deploy.
|
|
1197
|
+
if (revertReason(error) === "InvalidTerms") {
|
|
1198
|
+
return refuse(OpenerInvalidReason.invalidTerms, "InvalidTerms");
|
|
1199
|
+
}
|
|
1200
|
+
throw error;
|
|
1201
|
+
}
|
|
855
1202
|
if (
|
|
856
1203
|
!isAddress(requirements.payTo) ||
|
|
857
1204
|
getAddress(requirements.payTo) !== getAddress(predicted)
|
|
@@ -880,18 +1227,12 @@ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
|
880
1227
|
|
|
881
1228
|
const signed = auth as ExactEIP3009Payload & { signature: Hex };
|
|
882
1229
|
try {
|
|
883
|
-
await this.
|
|
884
|
-
account: this.config.walletClient.account,
|
|
885
|
-
address: binding.factory,
|
|
886
|
-
abi: x402StreamOpenerFactoryAbi,
|
|
887
|
-
functionName: "deployAndSettle",
|
|
888
|
-
args: [termsTuple(terms), authorizationTuple(signed)],
|
|
889
|
-
});
|
|
1230
|
+
await this.simulateDeployAndSettle(signed, plan);
|
|
890
1231
|
} catch (error) {
|
|
891
1232
|
return refuse(OpenerInvalidReason.simulationFailed, revertReason(error));
|
|
892
1233
|
}
|
|
893
1234
|
|
|
894
|
-
return { ok: true, auth: signed,
|
|
1235
|
+
return { ok: true, auth: signed, plan, opener: getAddress(predicted) };
|
|
895
1236
|
}
|
|
896
1237
|
}
|
|
897
1238
|
|
|
@@ -925,8 +1266,9 @@ function findStreamOpened(
|
|
|
925
1266
|
escrow: Address,
|
|
926
1267
|
auth: ExactEIP3009Payload,
|
|
927
1268
|
terms: OpenerTerms,
|
|
1269
|
+
/** Expected stream deposit; `undefined` matches ANY deposit (an absence check). */
|
|
1270
|
+
deposit: bigint | undefined,
|
|
928
1271
|
): { readonly streamId: Hex; readonly expiresAt: bigint } | undefined {
|
|
929
|
-
const value = BigInt(auth.authorization.value);
|
|
930
1272
|
const payer = getAddress(auth.authorization.from);
|
|
931
1273
|
const escrowStreams = new Map<string, bigint>();
|
|
932
1274
|
for (const log of logs) {
|
|
@@ -951,7 +1293,7 @@ function findStreamOpened(
|
|
|
951
1293
|
getAddress(args.buyer) === opener &&
|
|
952
1294
|
getAddress(args.operator) === getAddress(terms.operator) &&
|
|
953
1295
|
args.serviceRef.toLowerCase() === terms.serviceRef.toLowerCase() &&
|
|
954
|
-
args.deposit ===
|
|
1296
|
+
(deposit === undefined || args.deposit === deposit) &&
|
|
955
1297
|
args.ratePerSecond === terms.ratePerSecond
|
|
956
1298
|
) {
|
|
957
1299
|
escrowStreams.set(args.streamId.toLowerCase(), BigInt(args.expiresAt));
|
|
@@ -976,7 +1318,7 @@ function findStreamOpened(
|
|
|
976
1318
|
getAddress(args.payer) === payer &&
|
|
977
1319
|
args.authorizationNonce.toLowerCase() ===
|
|
978
1320
|
auth.authorization.nonce.toLowerCase() &&
|
|
979
|
-
args.deposit ===
|
|
1321
|
+
(deposit === undefined || args.deposit === deposit)
|
|
980
1322
|
) {
|
|
981
1323
|
return { streamId: args.streamId, expiresAt };
|
|
982
1324
|
}
|
|
@@ -987,6 +1329,54 @@ function findStreamOpened(
|
|
|
987
1329
|
return undefined;
|
|
988
1330
|
}
|
|
989
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* The opener's `UpfrontCharged` for this payer, authorization nonce and
|
|
1334
|
+
* operator, for exactly the terms' `upfrontAmount` (its `fee` is the protocol
|
|
1335
|
+
* fee taken out of it). `undefined` when the receipt carries none.
|
|
1336
|
+
*/
|
|
1337
|
+
function findUpfrontCharged(
|
|
1338
|
+
logs: readonly Log[],
|
|
1339
|
+
opener: Address,
|
|
1340
|
+
auth: ExactEIP3009Payload,
|
|
1341
|
+
terms: UpfrontOpenerTerms,
|
|
1342
|
+
):
|
|
1343
|
+
| { readonly streamId: Hex; readonly amount: bigint; readonly fee: bigint }
|
|
1344
|
+
| undefined {
|
|
1345
|
+
const payer = getAddress(auth.authorization.from);
|
|
1346
|
+
for (const log of logs) {
|
|
1347
|
+
if (getAddress(log.address) !== opener) continue;
|
|
1348
|
+
try {
|
|
1349
|
+
const decoded = decodeEventLog({
|
|
1350
|
+
abi: x402UpfrontStreamOpenerAbi,
|
|
1351
|
+
data: log.data,
|
|
1352
|
+
topics: log.topics,
|
|
1353
|
+
});
|
|
1354
|
+
if (decoded.eventName !== "UpfrontCharged") continue;
|
|
1355
|
+
const args = decoded.args as unknown as {
|
|
1356
|
+
authorizationNonce: Hex;
|
|
1357
|
+
payer: Address;
|
|
1358
|
+
operator: Address;
|
|
1359
|
+
amount: bigint;
|
|
1360
|
+
fee: bigint;
|
|
1361
|
+
streamId: Hex;
|
|
1362
|
+
};
|
|
1363
|
+
if (
|
|
1364
|
+
getAddress(args.payer) === payer &&
|
|
1365
|
+
args.authorizationNonce.toLowerCase() ===
|
|
1366
|
+
auth.authorization.nonce.toLowerCase() &&
|
|
1367
|
+
getAddress(args.operator) === getAddress(terms.operator) &&
|
|
1368
|
+
args.amount === terms.upfrontAmount &&
|
|
1369
|
+
args.fee <= args.amount
|
|
1370
|
+
) {
|
|
1371
|
+
return { streamId: args.streamId, amount: args.amount, fee: args.fee };
|
|
1372
|
+
}
|
|
1373
|
+
} catch {
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return undefined;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
990
1380
|
/**
|
|
991
1381
|
* Reads an opener facilitator marker out of a `/supported` kind's `extra`, or
|
|
992
1382
|
* `undefined` when the facilitator does not advertise opener settlement.
|
|
@@ -1023,6 +1413,34 @@ export function readOpenerFacilitatorMarker(
|
|
|
1023
1413
|
minAmount: marker.minAmount,
|
|
1024
1414
|
receiptTimeoutSeconds: marker.receiptTimeoutSeconds,
|
|
1025
1415
|
...(marker.refundRelay === true ? { refundRelay: true as const } : {}),
|
|
1416
|
+
...(readUpfrontMarker(marker.upfrontOpener) ?? {}),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** The optional `upfrontOpener` entry of a marker; a malformed one is ignored (absent). */
|
|
1421
|
+
function readUpfrontMarker(
|
|
1422
|
+
raw: unknown,
|
|
1423
|
+
): { readonly upfrontOpener: UpfrontOpenerMarker } | undefined {
|
|
1424
|
+
if (typeof raw !== "object" || raw === null) return undefined;
|
|
1425
|
+
const entry = raw as Record<string, unknown>;
|
|
1426
|
+
if (
|
|
1427
|
+
typeof entry.factory !== "string" ||
|
|
1428
|
+
!isAddress(entry.factory) ||
|
|
1429
|
+
typeof entry.implementation !== "string" ||
|
|
1430
|
+
!isAddress(entry.implementation) ||
|
|
1431
|
+
typeof entry.escrow !== "string" ||
|
|
1432
|
+
!isAddress(entry.escrow) ||
|
|
1433
|
+
entry.termsVersion !== OPENER_TERMS_VERSION.upfront
|
|
1434
|
+
) {
|
|
1435
|
+
return undefined;
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
upfrontOpener: {
|
|
1439
|
+
factory: getAddress(entry.factory),
|
|
1440
|
+
implementation: getAddress(entry.implementation),
|
|
1441
|
+
escrow: getAddress(entry.escrow),
|
|
1442
|
+
termsVersion: OPENER_TERMS_VERSION.upfront,
|
|
1443
|
+
},
|
|
1026
1444
|
};
|
|
1027
1445
|
}
|
|
1028
1446
|
|