@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 {
|
|
3
7
|
BaseError,
|
|
4
8
|
ContractFunctionRevertedError,
|
|
@@ -23,7 +27,7 @@ import {
|
|
|
23
27
|
|
|
24
28
|
import { x402StreamOpenerAbi, x402StreamOpenerFactoryAbi } from "./abi.js";
|
|
25
29
|
import { RelayerQueue } from "./relayer-queue.js";
|
|
26
|
-
import { chainIdFromNetwork } from "./terms.js";
|
|
30
|
+
import { chainIdFromNetwork, type OpenerKind } from "./terms.js";
|
|
27
31
|
import {
|
|
28
32
|
escrowGenerationFor,
|
|
29
33
|
readEscapeHatchReadyAt,
|
|
@@ -60,9 +64,18 @@ import {
|
|
|
60
64
|
*
|
|
61
65
|
* ## Bounded against gas abuse
|
|
62
66
|
*
|
|
63
|
-
* - Only streams whose escrow buyer is an opener of
|
|
64
|
-
* (`factory.isOpener(stream.buyer)`, on-chain
|
|
65
|
-
*
|
|
67
|
+
* - Only streams whose escrow buyer is an opener of a configured factory
|
|
68
|
+
* (`factory.isOpener(stream.buyer)`, on-chain: the stream factory, and the
|
|
69
|
+
* upfront factory when configured) are served; everything else is `404`
|
|
70
|
+
* before any signature check, oracle call or simulation. Which factory
|
|
71
|
+
* claims the opener decides its KIND, and so the EIP-712 domain its payer
|
|
72
|
+
* signs under ("Metrik X402StreamOpener" or "Metrik X402UpfrontStreamOpener").
|
|
73
|
+
* - Stream id 0 is an UPFRONT-ONLY payment (`upfrontAmount == price`): no
|
|
74
|
+
* stream exists and nothing was escrowed. Every route answers
|
|
75
|
+
* `nothing-to-refund` for it without a chain read, and no close or reclaim is
|
|
76
|
+
* ever submitted for it.
|
|
77
|
+
* - For an upfront opener, only the stream deposit (`price - upfrontAmount`)
|
|
78
|
+
* is refundable; the status reports that as `maxRefund`, never the price.
|
|
66
79
|
* - Every precondition the contract would check is read first (status, expiry,
|
|
67
80
|
* settlement window, reclaimed flag, non-zero refund). A request that would
|
|
68
81
|
* have no real effect never reaches the relayer.
|
|
@@ -84,6 +97,18 @@ import {
|
|
|
84
97
|
/** EIP-712 domain name/version of `X402StreamOpener`. Checked against the opener's ERC-5267 domain. */
|
|
85
98
|
export const OPENER_EIP712_NAME = "Metrik X402StreamOpener";
|
|
86
99
|
export const OPENER_EIP712_VERSION = "1";
|
|
100
|
+
/** EIP-712 domain name of `X402UpfrontStreamOpener` (same version, same types). */
|
|
101
|
+
export const UPFRONT_OPENER_EIP712_NAME = "Metrik X402UpfrontStreamOpener";
|
|
102
|
+
|
|
103
|
+
/** The EIP-712 domain name an opener of `kind` signs under. */
|
|
104
|
+
export function openerEip712Name(kind: OpenerKind): string {
|
|
105
|
+
return kind === "upfront" ? UPFRONT_OPENER_EIP712_NAME : OPENER_EIP712_NAME;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** True for the all-zero bytes32: an upfront-only payment, no stream. */
|
|
109
|
+
export function isNoStreamId(streamId: string): boolean {
|
|
110
|
+
return /^0x0{64}$/i.test(streamId);
|
|
111
|
+
}
|
|
87
112
|
|
|
88
113
|
/** The two payer-signed opener actions. */
|
|
89
114
|
export type OpenerSignedAction = "Close" | "ReclaimUnverified";
|
|
@@ -118,6 +143,8 @@ const CHECKPOINT_RETRY_HINT_SECONDS = 30;
|
|
|
118
143
|
*/
|
|
119
144
|
export function openerActionTypedData(input: {
|
|
120
145
|
readonly action: OpenerSignedAction;
|
|
146
|
+
/** The opener's kind (read on-chain); default `"stream"`. Picks the domain name. */
|
|
147
|
+
readonly kind?: OpenerKind;
|
|
121
148
|
readonly opener: Address;
|
|
122
149
|
readonly chainId: number;
|
|
123
150
|
readonly streamId: Hex;
|
|
@@ -126,7 +153,7 @@ export function openerActionTypedData(input: {
|
|
|
126
153
|
}) {
|
|
127
154
|
return {
|
|
128
155
|
domain: {
|
|
129
|
-
name:
|
|
156
|
+
name: openerEip712Name(input.kind ?? "stream"),
|
|
130
157
|
version: OPENER_EIP712_VERSION,
|
|
131
158
|
chainId: input.chainId,
|
|
132
159
|
verifyingContract: getAddress(input.opener),
|
|
@@ -178,6 +205,11 @@ export interface OpenerRefundRelayConfig {
|
|
|
178
205
|
/** CAIP-2 network (`eip155:<chainId>`), as the registry gives it. Must match the RPC's chain id. */
|
|
179
206
|
readonly network: string;
|
|
180
207
|
readonly factoryAddress: Address;
|
|
208
|
+
/**
|
|
209
|
+
* The `X402UpfrontStreamOpenerFactory` whose clones are served too (same
|
|
210
|
+
* escrow, checked by {@link OpenerRefundRelay.init}). Unset = stream openers only.
|
|
211
|
+
*/
|
|
212
|
+
readonly upfrontFactoryAddress?: Address;
|
|
181
213
|
/** The `StreamEscrowV2` the factory settles into (checked by {@link OpenerRefundRelay.init}). */
|
|
182
214
|
readonly escrowAddress: Address;
|
|
183
215
|
/** Base URL of the Metrik oracle that serves `GET /checkpoint/proof?streamId=`. */
|
|
@@ -290,6 +322,9 @@ interface ResolvedStream {
|
|
|
290
322
|
readonly stream: OpenerStream;
|
|
291
323
|
readonly opener: Address;
|
|
292
324
|
readonly payer: Address;
|
|
325
|
+
/** Which configured factory's `isOpener` claims `opener` (read on-chain). */
|
|
326
|
+
readonly kind: OpenerKind;
|
|
327
|
+
readonly factory: Address;
|
|
293
328
|
}
|
|
294
329
|
|
|
295
330
|
type OracleProof =
|
|
@@ -379,6 +414,7 @@ class MinuteWindows {
|
|
|
379
414
|
export class OpenerRefundRelay implements RefundRelayHandler {
|
|
380
415
|
private readonly chainId: number;
|
|
381
416
|
private readonly factory: Address;
|
|
417
|
+
private readonly upfrontFactory: Address | undefined;
|
|
382
418
|
private readonly escrow: Address;
|
|
383
419
|
private readonly oracleUrl: string;
|
|
384
420
|
private readonly queue: RelayerQueue;
|
|
@@ -409,6 +445,19 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
409
445
|
throw new Error("factoryAddress and escrowAddress must be EVM addresses");
|
|
410
446
|
}
|
|
411
447
|
this.factory = getAddress(config.factoryAddress);
|
|
448
|
+
if (
|
|
449
|
+
config.upfrontFactoryAddress !== undefined &&
|
|
450
|
+
!isAddress(config.upfrontFactoryAddress)
|
|
451
|
+
) {
|
|
452
|
+
throw new Error("upfrontFactoryAddress must be an EVM address");
|
|
453
|
+
}
|
|
454
|
+
this.upfrontFactory =
|
|
455
|
+
config.upfrontFactoryAddress === undefined
|
|
456
|
+
? undefined
|
|
457
|
+
: getAddress(config.upfrontFactoryAddress);
|
|
458
|
+
if (this.upfrontFactory === this.factory) {
|
|
459
|
+
throw new Error("upfrontFactoryAddress must differ from factoryAddress");
|
|
460
|
+
}
|
|
412
461
|
this.escrow = getAddress(config.escrowAddress);
|
|
413
462
|
const generation =
|
|
414
463
|
config.escrowGeneration ?? escrowGenerationFor(this.chainId, this.escrow);
|
|
@@ -487,6 +536,18 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
487
536
|
`refund relay: factory escrow ${factoryEscrow} is not the configured escrow ${this.escrow}`,
|
|
488
537
|
);
|
|
489
538
|
}
|
|
539
|
+
if (this.upfrontFactory !== undefined) {
|
|
540
|
+
const upfrontEscrow = await publicClient.readContract({
|
|
541
|
+
address: this.upfrontFactory,
|
|
542
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
543
|
+
functionName: "escrow",
|
|
544
|
+
});
|
|
545
|
+
if (getAddress(upfrontEscrow) !== this.escrow) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`refund relay: upfront factory escrow ${upfrontEscrow} is not the configured escrow ${this.escrow}`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
490
551
|
this.escapeGraceSeconds = BigInt(
|
|
491
552
|
await publicClient.readContract({
|
|
492
553
|
address: this.escrow,
|
|
@@ -501,6 +562,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
501
562
|
async status(rawStreamId: string): Promise<RefundRelayResponse> {
|
|
502
563
|
const streamId = parseStreamId(rawStreamId);
|
|
503
564
|
if (streamId === undefined) return invalidStreamId();
|
|
565
|
+
if (isNoStreamId(streamId)) return nothingToRefund(404);
|
|
504
566
|
const cached = this.fresh(this.statusCache, streamId);
|
|
505
567
|
if (cached !== undefined) return cached.response;
|
|
506
568
|
const response = await this.computeStatus(streamId);
|
|
@@ -517,12 +579,15 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
517
579
|
try {
|
|
518
580
|
const resolved = await this.resolve(rawStreamId);
|
|
519
581
|
if ("status" in resolved) return resolved;
|
|
520
|
-
const { streamId, stream, opener, payer } = resolved;
|
|
582
|
+
const { streamId, stream, opener, payer, kind, factory } = resolved;
|
|
521
583
|
const grace = this.grace();
|
|
522
|
-
const [now, nonce, latestCheckedAt] = await Promise.all([
|
|
584
|
+
const [now, nonce, latestCheckedAt, upfrontTerms] = await Promise.all([
|
|
523
585
|
this.chainNow(),
|
|
524
586
|
this.readNonce(opener, payer),
|
|
525
587
|
this.readLatestCheckedAt(),
|
|
588
|
+
kind === "upfront"
|
|
589
|
+
? this.readUpfrontTerms(opener)
|
|
590
|
+
: Promise.resolve(undefined),
|
|
526
591
|
]);
|
|
527
592
|
const closed = stream.status === 1;
|
|
528
593
|
const stopAt = stopTime(stream);
|
|
@@ -571,6 +636,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
571
636
|
typedData: jsonTypedData(
|
|
572
637
|
openerActionTypedData({
|
|
573
638
|
action: "Close",
|
|
639
|
+
kind,
|
|
574
640
|
opener,
|
|
575
641
|
chainId: this.chainId,
|
|
576
642
|
streamId,
|
|
@@ -617,6 +683,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
617
683
|
typedData: jsonTypedData(
|
|
618
684
|
openerActionTypedData({
|
|
619
685
|
action: "ReclaimUnverified",
|
|
686
|
+
kind,
|
|
620
687
|
opener,
|
|
621
688
|
chainId: this.chainId,
|
|
622
689
|
streamId,
|
|
@@ -643,12 +710,24 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
643
710
|
streamId,
|
|
644
711
|
chainId: this.chainId,
|
|
645
712
|
escrow: this.escrow,
|
|
646
|
-
factory
|
|
713
|
+
factory,
|
|
714
|
+
openerKind: kind,
|
|
647
715
|
opener,
|
|
648
716
|
payer,
|
|
649
717
|
operator: stream.operator,
|
|
650
718
|
serviceRef: stream.serviceRef,
|
|
651
719
|
deposit: stream.deposit.toString(),
|
|
720
|
+
// The most any refund of this payment can return: the escrowed
|
|
721
|
+
// deposit. For an upfront opener that is price - upfrontAmount; the
|
|
722
|
+
// upfront part was paid to the seller at settlement and is final.
|
|
723
|
+
maxRefund: stream.deposit.toString(),
|
|
724
|
+
...(upfrontTerms === undefined
|
|
725
|
+
? {}
|
|
726
|
+
: {
|
|
727
|
+
price: upfrontTerms.price.toString(),
|
|
728
|
+
upfrontAmount: upfrontTerms.upfrontAmount.toString(),
|
|
729
|
+
upfrontNote: `${upfrontTerms.upfrontAmount.toString()} of the ${upfrontTerms.price.toString()} paid was a non-refundable per-call charge, paid to the seller at settlement; at most the ${stream.deposit.toString()} escrowed can be refunded.`,
|
|
730
|
+
}),
|
|
652
731
|
ratePerSecond: stream.ratePerSecond.toString(),
|
|
653
732
|
claimedCumulative: stream.claimedCumulative.toString(),
|
|
654
733
|
openedAt: Number(stream.openedAt),
|
|
@@ -687,6 +766,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
687
766
|
): Promise<RefundRelayResponse> {
|
|
688
767
|
const streamId = parseStreamId(rawStreamId);
|
|
689
768
|
if (streamId === undefined) return invalidStreamId();
|
|
769
|
+
if (isNoStreamId(streamId)) return nothingToRefund(409);
|
|
690
770
|
const signed = parseSignedBody(body);
|
|
691
771
|
if ("status" in signed) return signed;
|
|
692
772
|
const key = ledgerKey("close", streamId);
|
|
@@ -695,7 +775,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
695
775
|
try {
|
|
696
776
|
const resolved = await this.resolve(streamId);
|
|
697
777
|
if ("status" in resolved) return resolved;
|
|
698
|
-
const { stream, opener, payer } = resolved;
|
|
778
|
+
const { stream, opener, payer, kind } = resolved;
|
|
699
779
|
const now = await this.chainNow();
|
|
700
780
|
if (stream.status === 1) {
|
|
701
781
|
return fail(409, "already-closed", "the stream is already closed", {
|
|
@@ -712,6 +792,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
712
792
|
}
|
|
713
793
|
const refused = await this.verifyPayerSignature(
|
|
714
794
|
"Close",
|
|
795
|
+
kind,
|
|
715
796
|
streamId,
|
|
716
797
|
opener,
|
|
717
798
|
payer,
|
|
@@ -740,6 +821,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
740
821
|
async reclaim(rawStreamId: string): Promise<RefundRelayResponse> {
|
|
741
822
|
const streamId = parseStreamId(rawStreamId);
|
|
742
823
|
if (streamId === undefined) return invalidStreamId();
|
|
824
|
+
if (isNoStreamId(streamId)) return nothingToRefund(409);
|
|
743
825
|
const key = ledgerKey("reclaim", streamId);
|
|
744
826
|
const early = await this.precheck(key, "reclaim");
|
|
745
827
|
if (early !== undefined) return early;
|
|
@@ -832,6 +914,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
832
914
|
): Promise<RefundRelayResponse> {
|
|
833
915
|
const streamId = parseStreamId(rawStreamId);
|
|
834
916
|
if (streamId === undefined) return invalidStreamId();
|
|
917
|
+
if (isNoStreamId(streamId)) return nothingToRefund(409);
|
|
835
918
|
const signed = parseSignedBody(body);
|
|
836
919
|
if ("status" in signed) return signed;
|
|
837
920
|
const key = ledgerKey("reclaim", streamId);
|
|
@@ -840,7 +923,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
840
923
|
try {
|
|
841
924
|
const resolved = await this.resolve(streamId);
|
|
842
925
|
if ("status" in resolved) return resolved;
|
|
843
|
-
const { stream, opener, payer } = resolved;
|
|
926
|
+
const { stream, opener, payer, kind } = resolved;
|
|
844
927
|
if (stream.reclaimed) return alreadyReclaimed();
|
|
845
928
|
const now = await this.chainNow();
|
|
846
929
|
const escapeAt = await this.escapeHatchAt(streamId, stream);
|
|
@@ -868,6 +951,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
868
951
|
}
|
|
869
952
|
const refused = await this.verifyPayerSignature(
|
|
870
953
|
"ReclaimUnverified",
|
|
954
|
+
kind,
|
|
871
955
|
streamId,
|
|
872
956
|
opener,
|
|
873
957
|
payer,
|
|
@@ -1236,13 +1320,31 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
1236
1320
|
}
|
|
1237
1321
|
if (!isAddress(stream.buyer)) return notFound();
|
|
1238
1322
|
const buyer = getAddress(stream.buyer);
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1323
|
+
// The kind is whichever CONFIGURED factory's isOpener claims the buyer,
|
|
1324
|
+
// read on-chain — never inferred from the address, code or the request.
|
|
1325
|
+
const [isStreamOpener, isUpfrontOpener] = await Promise.all([
|
|
1326
|
+
publicClient.readContract({
|
|
1327
|
+
address: this.factory,
|
|
1328
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
1329
|
+
functionName: "isOpener",
|
|
1330
|
+
args: [buyer],
|
|
1331
|
+
}),
|
|
1332
|
+
this.upfrontFactory === undefined
|
|
1333
|
+
? Promise.resolve(false)
|
|
1334
|
+
: publicClient.readContract({
|
|
1335
|
+
address: this.upfrontFactory,
|
|
1336
|
+
abi: x402UpfrontStreamOpenerFactoryAbi,
|
|
1337
|
+
functionName: "isOpener",
|
|
1338
|
+
args: [buyer],
|
|
1339
|
+
}),
|
|
1340
|
+
]);
|
|
1341
|
+
if ((isStreamOpener === true) === (isUpfrontOpener === true)) {
|
|
1342
|
+
// Neither (not our opener) — or both, which CREATE2 makes impossible.
|
|
1343
|
+
return notFound();
|
|
1344
|
+
}
|
|
1345
|
+
const kind: OpenerKind = isUpfrontOpener === true ? "upfront" : "stream";
|
|
1346
|
+
const factory =
|
|
1347
|
+
kind === "upfront" ? (this.upfrontFactory as Address) : this.factory;
|
|
1246
1348
|
const payer = await publicClient.readContract({
|
|
1247
1349
|
address: buyer,
|
|
1248
1350
|
abi: x402StreamOpenerAbi,
|
|
@@ -1254,6 +1356,8 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
1254
1356
|
streamId,
|
|
1255
1357
|
opener: buyer,
|
|
1256
1358
|
payer: getAddress(payer),
|
|
1359
|
+
kind,
|
|
1360
|
+
factory,
|
|
1257
1361
|
stream: {
|
|
1258
1362
|
buyer,
|
|
1259
1363
|
operator: getAddress(stream.operator),
|
|
@@ -1274,6 +1378,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
1274
1378
|
/** Off-chain check of a payer-signed opener action. `undefined` = valid. */
|
|
1275
1379
|
private async verifyPayerSignature(
|
|
1276
1380
|
action: OpenerSignedAction,
|
|
1381
|
+
kind: OpenerKind,
|
|
1277
1382
|
streamId: Hex,
|
|
1278
1383
|
opener: Address,
|
|
1279
1384
|
payer: Address,
|
|
@@ -1322,7 +1427,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
1322
1427
|
readonly bigint[],
|
|
1323
1428
|
];
|
|
1324
1429
|
if (
|
|
1325
|
-
name !==
|
|
1430
|
+
name !== openerEip712Name(kind) ||
|
|
1326
1431
|
version !== OPENER_EIP712_VERSION ||
|
|
1327
1432
|
BigInt(chainId) !== BigInt(this.chainId) ||
|
|
1328
1433
|
getAddress(verifyingContract) !== opener
|
|
@@ -1340,6 +1445,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
1340
1445
|
address: payer,
|
|
1341
1446
|
...openerActionTypedData({
|
|
1342
1447
|
action,
|
|
1448
|
+
kind,
|
|
1343
1449
|
opener,
|
|
1344
1450
|
chainId: this.chainId,
|
|
1345
1451
|
streamId,
|
|
@@ -1380,6 +1486,21 @@ export class OpenerRefundRelay implements RefundRelayHandler {
|
|
|
1380
1486
|
);
|
|
1381
1487
|
}
|
|
1382
1488
|
|
|
1489
|
+
/** An upfront opener's immutable terms (`price`, `upfrontAmount`). */
|
|
1490
|
+
private async readUpfrontTerms(
|
|
1491
|
+
opener: Address,
|
|
1492
|
+
): Promise<{ readonly price: bigint; readonly upfrontAmount: bigint }> {
|
|
1493
|
+
const terms = (await this.config.publicClient.readContract({
|
|
1494
|
+
address: opener,
|
|
1495
|
+
abi: x402UpfrontStreamOpenerAbi,
|
|
1496
|
+
functionName: "terms",
|
|
1497
|
+
})) as unknown as { price: bigint; upfrontAmount: bigint };
|
|
1498
|
+
return {
|
|
1499
|
+
price: BigInt(terms.price),
|
|
1500
|
+
upfrontAmount: BigInt(terms.upfrontAmount),
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1383
1504
|
private async readLatestCheckedAt(): Promise<bigint> {
|
|
1384
1505
|
return BigInt(
|
|
1385
1506
|
await this.config.publicClient.readContract({
|
|
@@ -1672,7 +1793,16 @@ function notFound(): RefundRelayResponse {
|
|
|
1672
1793
|
return fail(
|
|
1673
1794
|
404,
|
|
1674
1795
|
"not-found",
|
|
1675
|
-
"no stream opened through this facilitator's
|
|
1796
|
+
"no stream opened through this facilitator's opener factories has that id",
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/** Stream id 0: an upfront-only payment. Never read, closed or reclaimed. */
|
|
1801
|
+
function nothingToRefund(status: 404 | 409): RefundRelayResponse {
|
|
1802
|
+
return fail(
|
|
1803
|
+
status,
|
|
1804
|
+
"nothing-to-refund",
|
|
1805
|
+
"stream id 0 is an upfront-only payment (upfrontAmount == price): the whole price was paid to the seller at settlement, no stream was opened, nothing was escrowed, and there is nothing to close or reclaim",
|
|
1676
1806
|
);
|
|
1677
1807
|
}
|
|
1678
1808
|
|