@absol-labs/agent 0.11.0 → 0.12.0-rc.1
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 +42 -50
- package/dist/capability/invocation-capability.d.ts +2 -2
- package/dist/capability/probe-capability.d.ts +225 -0
- package/dist/capability/probe-capability.d.ts.map +1 -0
- package/dist/capability/probe-capability.js +248 -0
- package/dist/capability/probe-capability.js.map +1 -0
- package/dist/cli/args.d.ts +11 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/claim.js +1 -1
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts.map +1 -1
- package/dist/cli/commands/close.js +35 -3
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -1
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/invoke.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +15 -2
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/open.d.ts.map +1 -1
- package/dist/cli/commands/open.js +58 -4
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +19 -0
- package/dist/cli/commands/pay.d.ts.map +1 -0
- package/dist/cli/commands/pay.js +355 -0
- package/dist/cli/commands/pay.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
- package/dist/cli/commands/reclaim-unverified.js +3 -2
- package/dist/cli/commands/reclaim-unverified.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts +23 -2
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +184 -16
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +13 -0
- package/dist/cli/commands/refund.d.ts.map +1 -0
- package/dist/cli/commands/refund.js +224 -0
- package/dist/cli/commands/refund.js.map +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +10 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/streams.js +8 -12
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -1
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/config.d.ts +44 -26
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +88 -38
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/deps.d.ts +33 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +37 -1
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js +21 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/escape-hatch.d.ts +6 -4
- package/dist/cli/escape-hatch.d.ts.map +1 -1
- package/dist/cli/escape-hatch.js +6 -2
- package/dist/cli/escape-hatch.js.map +1 -1
- package/dist/cli/listings.d.ts +18 -0
- package/dist/cli/listings.d.ts.map +1 -1
- package/dist/cli/listings.js +22 -12
- package/dist/cli/listings.js.map +1 -1
- package/dist/cli/preflight.d.ts +20 -14
- package/dist/cli/preflight.d.ts.map +1 -1
- package/dist/cli/preflight.js +34 -20
- package/dist/cli/preflight.js.map +1 -1
- package/dist/cli/privy.d.ts.map +1 -1
- package/dist/cli/privy.js +4 -2
- package/dist/cli/privy.js.map +1 -1
- package/dist/cli/reclaimability.d.ts +82 -0
- package/dist/cli/reclaimability.d.ts.map +1 -0
- package/dist/cli/reclaimability.js +80 -0
- package/dist/cli/reclaimability.js.map +1 -0
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +40 -11
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/scan-range.d.ts +2 -2
- package/dist/cli/scan-range.js +2 -2
- package/dist/cli/status.d.ts +3 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +15 -3
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +1 -1
- package/dist/cli/wallet.js +2 -2
- package/dist/cli/wallet.js.map +1 -1
- package/dist/discovery/rate-band.d.ts +74 -0
- package/dist/discovery/rate-band.d.ts.map +1 -0
- package/dist/discovery/rate-band.js +114 -0
- package/dist/discovery/rate-band.js.map +1 -0
- package/dist/escrow-generation.d.ts +81 -0
- package/dist/escrow-generation.d.ts.map +1 -0
- package/dist/escrow-generation.js +76 -0
- package/dist/escrow-generation.js.map +1 -0
- package/dist/frameworks/agentkit.d.ts +8 -4
- package/dist/frameworks/agentkit.d.ts.map +1 -1
- package/dist/frameworks/agentkit.js +27 -10
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/frameworks/eliza.d.ts +3 -3
- package/dist/frameworks/eliza.d.ts.map +1 -1
- package/dist/frameworks/eliza.js +15 -6
- package/dist/frameworks/eliza.js.map +1 -1
- package/dist/frameworks/langchain.d.ts.map +1 -1
- package/dist/frameworks/langchain.js +8 -0
- package/dist/frameworks/langchain.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +83 -31
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +180 -27
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +111 -44
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +439 -155
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/probe-auth.d.ts +112 -0
- package/dist/gateway/probe-auth.d.ts.map +1 -0
- package/dist/gateway/probe-auth.js +127 -0
- package/dist/gateway/probe-auth.js.map +1 -0
- package/dist/gateway/signed-rate-bounds.d.ts +51 -0
- package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
- package/dist/gateway/signed-rate-bounds.js +94 -0
- package/dist/gateway/signed-rate-bounds.js.map +1 -0
- package/dist/gateway/x402-opener-mode.d.ts +215 -0
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
- package/dist/gateway/x402-opener-mode.js +639 -0
- package/dist/gateway/x402-opener-mode.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +52 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +228 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +104 -0
- package/dist/mcp/x402-tools.d.ts.map +1 -0
- package/dist/mcp/x402-tools.js +434 -0
- package/dist/mcp/x402-tools.js.map +1 -0
- package/dist/network.d.ts +119 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +199 -0
- package/dist/network.js.map +1 -0
- package/dist/sdk/invoke.d.ts +4 -0
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +2 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
- package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.js +27 -23
- package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
- package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.js +9 -2
- package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet.js +3 -2
- package/dist/wallet/autonomous-wallet.js.map +1 -1
- package/dist/wallet/broker-escrows.d.ts +222 -0
- package/dist/wallet/broker-escrows.d.ts.map +1 -0
- package/dist/wallet/broker-escrows.js +36 -0
- package/dist/wallet/broker-escrows.js.map +1 -0
- package/dist/wallet/lifecycle.d.ts +21 -5
- package/dist/wallet/lifecycle.d.ts.map +1 -1
- package/dist/wallet/lifecycle.js +32 -12
- package/dist/wallet/lifecycle.js.map +1 -1
- package/dist/wallet/privy-broker-server.d.ts.map +1 -1
- package/dist/wallet/privy-broker-server.js +31 -26
- package/dist/wallet/privy-broker-server.js.map +1 -1
- package/dist/wallet/privy-session-broker.d.ts +14 -5
- package/dist/wallet/privy-session-broker.d.ts.map +1 -1
- package/dist/wallet/privy-session-broker.js +29 -17
- package/dist/wallet/privy-session-broker.js.map +1 -1
- package/dist/wallet/privy-session-provider.d.ts +3 -1
- package/dist/wallet/privy-session-provider.d.ts.map +1 -1
- package/dist/wallet/privy-session-provider.js +5 -3
- package/dist/wallet/privy-session-provider.js.map +1 -1
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +15 -8
- package/dist/wallet/provider.js.map +1 -1
- package/dist/x402/facilitator.d.ts +3 -30
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +0 -27
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
- package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
- package/dist/x402-buyer/guarded-fetch.js +147 -0
- package/dist/x402-buyer/guarded-fetch.js.map +1 -0
- package/dist/x402-buyer/index.d.ts +10 -0
- package/dist/x402-buyer/index.d.ts.map +1 -0
- package/dist/x402-buyer/index.js +10 -0
- package/dist/x402-buyer/index.js.map +1 -0
- package/dist/x402-buyer/pay.d.ts +135 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -0
- package/dist/x402-buyer/pay.js +336 -0
- package/dist/x402-buyer/pay.js.map +1 -0
- package/dist/x402-opener/abi.d.ts +684 -0
- package/dist/x402-opener/abi.d.ts.map +1 -0
- package/dist/x402-opener/abi.js +880 -0
- package/dist/x402-opener/abi.js.map +1 -0
- package/dist/x402-opener/facilitator-entry.d.ts +3 -0
- package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-entry.js +37 -0
- package/dist/x402-opener/facilitator-entry.js.map +1 -0
- package/dist/x402-opener/facilitator-server.d.ts +180 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-server.js +696 -0
- package/dist/x402-opener/facilitator-server.js.map +1 -0
- package/dist/x402-opener/index.d.ts +14 -0
- package/dist/x402-opener/index.d.ts.map +1 -0
- package/dist/x402-opener/index.js +14 -0
- package/dist/x402-opener/index.js.map +1 -0
- package/dist/x402-opener/listing-resolver.d.ts +55 -0
- package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
- package/dist/x402-opener/listing-resolver.js +125 -0
- package/dist/x402-opener/listing-resolver.js.map +1 -0
- package/dist/x402-opener/refund-relay.d.ts +240 -0
- package/dist/x402-opener/refund-relay.d.ts.map +1 -0
- package/dist/x402-opener/refund-relay.js +1240 -0
- package/dist/x402-opener/refund-relay.js.map +1 -0
- package/dist/x402-opener/relayer-queue.d.ts +14 -0
- package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
- package/dist/x402-opener/relayer-queue.js +18 -0
- package/dist/x402-opener/relayer-queue.js.map +1 -0
- package/dist/x402-opener/scheme.d.ts +217 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -0
- package/dist/x402-opener/scheme.js +749 -0
- package/dist/x402-opener/scheme.js.map +1 -0
- package/dist/x402-opener/terms.d.ts +105 -0
- package/dist/x402-opener/terms.d.ts.map +1 -0
- package/dist/x402-opener/terms.js +212 -0
- package/dist/x402-opener/terms.js.map +1 -0
- package/dist/zktls/reclaim.d.ts +2 -2
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +3 -2
- package/docs/autonomous-privy-wallet.md +188 -0
- package/docs/cli.md +184 -31
- package/docs/langchain.md +5 -0
- package/docs/mcp-reference.md +75 -5
- package/docs/probe-capability.md +191 -0
- package/docs/quickstart.md +36 -1
- package/docs/threat-model.md +20 -0
- package/docs/x402.md +628 -595
- package/package.json +19 -17
- package/src/capability/probe-capability.ts +355 -0
- package/src/cli/args.ts +29 -0
- package/src/cli/commands/claim.ts +1 -1
- package/src/cli/commands/close.ts +36 -3
- package/src/cli/commands/invoke.ts +5 -2
- package/src/cli/commands/mcp.ts +18 -2
- package/src/cli/commands/open.ts +75 -4
- package/src/cli/commands/pay.ts +442 -0
- package/src/cli/commands/reclaim-unverified.ts +8 -2
- package/src/cli/commands/reclaim.ts +252 -14
- package/src/cli/commands/refund.ts +277 -0
- package/src/cli/commands/status.ts +17 -1
- package/src/cli/commands/streams.ts +10 -14
- package/src/cli/commands/watch.ts +7 -1
- package/src/cli/config.ts +139 -52
- package/src/cli/deps.ts +74 -1
- package/src/cli/errors.ts +24 -1
- package/src/cli/escape-hatch.ts +19 -5
- package/src/cli/listings.ts +43 -10
- package/src/cli/preflight.ts +36 -21
- package/src/cli/privy.ts +4 -2
- package/src/cli/reclaimability.ts +154 -0
- package/src/cli/run.ts +54 -11
- package/src/cli/scan-range.ts +2 -2
- package/src/cli/status.ts +14 -1
- package/src/cli/wallet.ts +2 -2
- package/src/discovery/rate-band.ts +172 -0
- package/src/escrow-generation.ts +135 -0
- package/src/frameworks/agentkit.ts +45 -11
- package/src/frameworks/eliza.ts +27 -5
- package/src/frameworks/langchain.ts +14 -0
- package/src/gateway/caller-auth-gateway.ts +320 -68
- package/src/gateway/http-server.ts +629 -220
- package/src/gateway/probe-auth.ts +191 -0
- package/src/gateway/signed-rate-bounds.ts +164 -0
- package/src/gateway/x402-opener-mode.ts +974 -0
- package/src/index.ts +82 -19
- package/src/mcp/server.ts +319 -42
- package/src/mcp/x402-tools.ts +621 -0
- package/src/network.ts +302 -0
- package/src/sdk/invoke.ts +6 -5
- package/src/wallet/autonomous-wallet-broker.ts +46 -20
- package/src/wallet/autonomous-wallet-protocol.ts +11 -2
- package/src/wallet/autonomous-wallet.ts +4 -2
- package/src/wallet/broker-escrows.ts +66 -0
- package/src/wallet/lifecycle.ts +53 -18
- package/src/wallet/privy-broker-server.ts +35 -25
- package/src/wallet/privy-session-broker.ts +58 -25
- package/src/wallet/privy-session-provider.ts +7 -3
- package/src/wallet/provider.ts +20 -15
- package/src/x402/facilitator.ts +3 -43
- package/src/x402-buyer/guarded-fetch.ts +213 -0
- package/src/x402-buyer/index.ts +9 -0
- package/src/x402-buyer/pay.ts +517 -0
- package/src/x402-opener/abi.ts +881 -0
- package/src/x402-opener/facilitator-entry.ts +45 -0
- package/src/x402-opener/facilitator-server.ts +1014 -0
- package/src/x402-opener/index.ts +13 -0
- package/src/x402-opener/listing-resolver.ts +202 -0
- package/src/x402-opener/refund-relay.ts +1743 -0
- package/src/x402-opener/relayer-queue.ts +18 -0
- package/src/x402-opener/scheme.ts +1133 -0
- package/src/x402-opener/terms.ts +300 -0
- package/dist/gateway/x402-mode.d.ts +0 -123
- package/dist/gateway/x402-mode.d.ts.map +0 -1
- package/dist/gateway/x402-mode.js +0 -212
- package/dist/gateway/x402-mode.js.map +0 -1
- package/dist/x402/delivery-oracle.d.ts +0 -108
- package/dist/x402/delivery-oracle.d.ts.map +0 -1
- package/dist/x402/delivery-oracle.js +0 -174
- package/dist/x402/delivery-oracle.js.map +0 -1
- package/dist/x402/facilitator-server-entry.d.ts +0 -3
- package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
- package/dist/x402/facilitator-server-entry.js +0 -27
- package/dist/x402/facilitator-server-entry.js.map +0 -1
- package/dist/x402/facilitator-server.d.ts +0 -73
- package/dist/x402/facilitator-server.d.ts.map +0 -1
- package/dist/x402/facilitator-server.js +0 -305
- package/dist/x402/facilitator-server.js.map +0 -1
- package/dist/x402/payment-requirements.d.ts +0 -106
- package/dist/x402/payment-requirements.d.ts.map +0 -1
- package/dist/x402/payment-requirements.js +0 -138
- package/dist/x402/payment-requirements.js.map +0 -1
- package/dist/x402/relayer.d.ts +0 -41
- package/dist/x402/relayer.d.ts.map +0 -1
- package/dist/x402/relayer.js +0 -64
- package/dist/x402/relayer.js.map +0 -1
- package/dist/x402/resource-index.d.ts +0 -75
- package/dist/x402/resource-index.d.ts.map +0 -1
- package/dist/x402/resource-index.js +0 -126
- package/dist/x402/resource-index.js.map +0 -1
- package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
- package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
- package/dist/x402/verified-delivery-facilitator.js +0 -423
- package/dist/x402/verified-delivery-facilitator.js.map +0 -1
- package/src/gateway/x402-mode.ts +0 -350
- package/src/x402/delivery-oracle.ts +0 -285
- package/src/x402/facilitator-server-entry.ts +0 -35
- package/src/x402/facilitator-server.ts +0 -445
- package/src/x402/payment-requirements.ts +0 -231
- package/src/x402/relayer.ts +0 -122
- package/src/x402/resource-index.ts +0 -228
- package/src/x402/verified-delivery-facilitator.ts +0 -744
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import { streamEscrowV2Abi } from "@absol-labs/shared";
|
|
2
|
+
import type {
|
|
3
|
+
FacilitatorContext,
|
|
4
|
+
Network,
|
|
5
|
+
PaymentPayload,
|
|
6
|
+
PaymentRequirements,
|
|
7
|
+
SchemeNetworkFacilitator,
|
|
8
|
+
SettleResponse,
|
|
9
|
+
VerifyResponse,
|
|
10
|
+
} from "@x402/core/types";
|
|
11
|
+
import { deepEqual } from "@x402/core/utils";
|
|
12
|
+
import {
|
|
13
|
+
isEIP3009Payload,
|
|
14
|
+
toFacilitatorEvmSigner,
|
|
15
|
+
type ExactEIP3009Payload,
|
|
16
|
+
type ExactEvmPayloadV2,
|
|
17
|
+
} from "@x402/evm";
|
|
18
|
+
import { ExactEvmScheme } from "@x402/evm/exact/facilitator";
|
|
19
|
+
import {
|
|
20
|
+
BaseError,
|
|
21
|
+
ContractFunctionRevertedError,
|
|
22
|
+
decodeEventLog,
|
|
23
|
+
getAddress,
|
|
24
|
+
isAddress,
|
|
25
|
+
isErc6492Signature,
|
|
26
|
+
isHex,
|
|
27
|
+
type Account,
|
|
28
|
+
type Address,
|
|
29
|
+
type Chain,
|
|
30
|
+
type Hex,
|
|
31
|
+
type Log,
|
|
32
|
+
type PublicClient,
|
|
33
|
+
type Transport,
|
|
34
|
+
type WalletClient,
|
|
35
|
+
} from "viem";
|
|
36
|
+
|
|
37
|
+
import { x402StreamOpenerAbi, x402StreamOpenerFactoryAbi } from "./abi.js";
|
|
38
|
+
import type { OperatorSignedListingResolver } from "./listing-resolver.js";
|
|
39
|
+
import { RelayerQueue } from "./relayer-queue.js";
|
|
40
|
+
import {
|
|
41
|
+
chainIdFromNetwork,
|
|
42
|
+
maxEarnable,
|
|
43
|
+
OPENER_EXTRA_KEY,
|
|
44
|
+
OPENER_SCHEME,
|
|
45
|
+
parseUintString,
|
|
46
|
+
termsFromExtra,
|
|
47
|
+
termsTuple,
|
|
48
|
+
type OpenerTerms,
|
|
49
|
+
} from "./terms.js";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `MetrikOpenerExactScheme` — an x402 v2 `exact` facilitator scheme that
|
|
53
|
+
* settles every payment INTO a `StreamEscrowV2` stream through the ownerless
|
|
54
|
+
* `X402StreamOpenerFactory` (metrik-agent#116, metrik-contracts#48).
|
|
55
|
+
*
|
|
56
|
+
* ## What it composes, and what it refuses
|
|
57
|
+
*
|
|
58
|
+
* Standard `exact` checks (EIP-712 signature, recipient == `payTo`, value ==
|
|
59
|
+
* amount, validity window, asset is a contract, balance / nonce simulation of
|
|
60
|
+
* the transfer) are delegated to the official `@x402/evm` `ExactEvmScheme`
|
|
61
|
+
* verifier, never reimplemented. On top of it, every payment must satisfy:
|
|
62
|
+
*
|
|
63
|
+
* - `assetTransferMethod` is EIP-3009. Permit2 and every other method are
|
|
64
|
+
* refused: the opener only ever pulls an EIP-3009 authorization.
|
|
65
|
+
* - The signature is NOT ERC-6492-wrapped. A counterfactual smart wallet would
|
|
66
|
+
* need a factory deploy the opener cannot perform; refused with a reason.
|
|
67
|
+
* - `asset` is on the allowlist AND is the factory's own settlement token.
|
|
68
|
+
* - `payTo == factory.predict(terms)`, recomputed on-chain from the configured
|
|
69
|
+
* factory for the terms in `extra.metrikStreamOpener` — never compared by
|
|
70
|
+
* code or codehash (a byte-identical clone outside the factory proves
|
|
71
|
+
* nothing). The factory named in `extra` must be the configured one.
|
|
72
|
+
* - `(operator, serviceRef)` resolve to exactly ONE operator-signed listing via
|
|
73
|
+
* the existing discovery signature verification; the rate is inside the
|
|
74
|
+
* listing's signed commercial bounds when it declares them.
|
|
75
|
+
* - `0 < amount <= ratePerSecond * maxDuration`.
|
|
76
|
+
* - `factory.deployAndSettle` simulates cleanly from the relayer.
|
|
77
|
+
*
|
|
78
|
+
* Any doubt — an RPC error, an unreachable registry, a malformed field — is an
|
|
79
|
+
* invalid verdict. Nothing here ever fails open.
|
|
80
|
+
*
|
|
81
|
+
* ## Settlement
|
|
82
|
+
*
|
|
83
|
+
* `settle` re-runs the full verification, then calls `deployAndSettle` from the
|
|
84
|
+
* relayer, waits for the receipt, and returns success ONLY when the mined
|
|
85
|
+
* receipt carries both the opener's `StreamOpenedFor` and the escrow's
|
|
86
|
+
* `StreamOpened` for exactly this payer, nonce, amount and terms. The stock
|
|
87
|
+
* `ExactEvmScheme.settle` is never called: it would transfer the USDC straight
|
|
88
|
+
* to `payTo`, and USDC that reaches an opener any other way than
|
|
89
|
+
* `settleAndOpen` is unrecoverable. The signer handed to the stock scheme
|
|
90
|
+
* cannot broadcast at all, so that path is closed structurally too.
|
|
91
|
+
*
|
|
92
|
+
* ## At most one success per authorization
|
|
93
|
+
*
|
|
94
|
+
* An EIP-3009 authorization (chain + asset + payer + nonce) receives AT MOST
|
|
95
|
+
* ONE successful `SettleResponse` from this instance, ever. A settle of an
|
|
96
|
+
* authorization that is in flight, or already settled, returns `success:
|
|
97
|
+
* false` with `duplicate_settlement` (carrying the tx hash when there is one),
|
|
98
|
+
* and `verify` refuses it the same way — so one payment can never buy two
|
|
99
|
+
* served responses. After a `settlement_pending` (receipt wait timed out) the
|
|
100
|
+
* next settle of the SAME payload reconciles against the broadcast tx and may
|
|
101
|
+
* receive the one success, if none was delivered before. The ledger is
|
|
102
|
+
* in-memory and per instance: a multi-instance deployment needs a shared store
|
|
103
|
+
* and does not have one today.
|
|
104
|
+
*
|
|
105
|
+
* Relayer submissions (simulate + write) are serialized so concurrent payers
|
|
106
|
+
* never race for the relayer's account nonce.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/** Reasons this scheme adds to the stock `exact` ones. All fail closed. */
|
|
110
|
+
export const OpenerInvalidReason = {
|
|
111
|
+
unsupportedScheme: "metrik_opener_unsupported_scheme",
|
|
112
|
+
unsupportedNetwork: "metrik_opener_unsupported_network",
|
|
113
|
+
unsupportedVersion: "metrik_opener_unsupported_x402_version",
|
|
114
|
+
acceptedMismatch: "metrik_opener_accepted_requirements_mismatch",
|
|
115
|
+
unsupportedAssetTransferMethod:
|
|
116
|
+
"metrik_opener_asset_transfer_method_not_eip3009",
|
|
117
|
+
erc6492Unsupported: "metrik_opener_erc6492_signature_unsupported",
|
|
118
|
+
missingSignature: "metrik_opener_missing_signature",
|
|
119
|
+
assetNotAllowed: "metrik_opener_asset_not_allowed",
|
|
120
|
+
invalidTerms: "metrik_opener_invalid_terms",
|
|
121
|
+
factoryMismatch: "metrik_opener_factory_mismatch",
|
|
122
|
+
payToMismatch: "metrik_opener_pay_to_not_predicted_opener",
|
|
123
|
+
invalidAmount: "metrik_opener_invalid_amount",
|
|
124
|
+
amountExceedsTerms: "metrik_opener_amount_exceeds_rate_times_duration",
|
|
125
|
+
listingUnavailable: "metrik_opener_listing_registry_unavailable",
|
|
126
|
+
listingNotFound: "metrik_opener_listing_not_found",
|
|
127
|
+
listingAmbiguous: "metrik_opener_listing_ambiguous",
|
|
128
|
+
listingRateOutOfBounds: "metrik_opener_listing_rate_out_of_bounds",
|
|
129
|
+
simulationFailed: "metrik_opener_settle_simulation_failed",
|
|
130
|
+
verifyError: "metrik_opener_verify_error",
|
|
131
|
+
transactionReverted: "metrik_opener_transaction_reverted",
|
|
132
|
+
streamEventMismatch: "metrik_opener_stream_event_mismatch",
|
|
133
|
+
settlementPending: "settlement_pending",
|
|
134
|
+
settleError: "metrik_opener_settle_error",
|
|
135
|
+
amountBelowMinimum: "metrik_opener_amount_below_minimum",
|
|
136
|
+
/**
|
|
137
|
+
* This authorization is already being settled, or already settled, by this
|
|
138
|
+
* facilitator instance. May carry the settlement transaction hash. Never a
|
|
139
|
+
* second success (see "At most one success" on the class).
|
|
140
|
+
*/
|
|
141
|
+
duplicateSettlement: "duplicate_settlement",
|
|
142
|
+
settlementCapacity: "metrik_opener_settlement_capacity_exhausted",
|
|
143
|
+
} as const;
|
|
144
|
+
|
|
145
|
+
/** Default facilitator minimum payment: 10000 base units = 0.01 USDC. */
|
|
146
|
+
export const DEFAULT_OPENER_MIN_AMOUNT = 10_000n;
|
|
147
|
+
|
|
148
|
+
/** Hard cap on remembered authorizations; beyond it new settles fail closed. */
|
|
149
|
+
const MAX_LEDGER_ENTRIES = 100_000;
|
|
150
|
+
|
|
151
|
+
/** Grace after an authorization's `validBefore` before its ledger entry is dropped. */
|
|
152
|
+
const LEDGER_GRACE_SECONDS = 600n;
|
|
153
|
+
|
|
154
|
+
export interface MetrikOpenerExactSchemeConfig {
|
|
155
|
+
/** CAIP-2 network (`eip155:<chainId>`), as the registry gives it. Must match the RPC's chain id. */
|
|
156
|
+
readonly network: Network;
|
|
157
|
+
/** The deployed `X402StreamOpenerFactory`. */
|
|
158
|
+
readonly factoryAddress: Address;
|
|
159
|
+
/** The `StreamEscrowV2` the factory must point at (checked by {@link init}). */
|
|
160
|
+
readonly escrowAddress: Address;
|
|
161
|
+
/** Settlement tokens this facilitator accepts. Must contain `factory.usdc()`. */
|
|
162
|
+
readonly assetAllowlist: readonly Address[];
|
|
163
|
+
/** Read client for the network. */
|
|
164
|
+
readonly publicClient: PublicClient<Transport, Chain | undefined>;
|
|
165
|
+
/** Relayer wallet. Pays gas for `deployAndSettle`; never holds buyer funds. */
|
|
166
|
+
readonly walletClient: WalletClient<Transport, Chain | undefined, Account>;
|
|
167
|
+
readonly listingResolver: OperatorSignedListingResolver;
|
|
168
|
+
/** How long to wait for a settlement receipt before reporting `settlement_pending`. Default 60s. */
|
|
169
|
+
readonly receiptTimeoutMs?: number;
|
|
170
|
+
/**
|
|
171
|
+
* Smallest accepted payment, token base units. Default
|
|
172
|
+
* {@link DEFAULT_OPENER_MIN_AMOUNT}. Every settlement costs the relayer a
|
|
173
|
+
* full `deployAndSettle`, so dust payments would be a gas drain.
|
|
174
|
+
*/
|
|
175
|
+
readonly minAmount?: bigint;
|
|
176
|
+
/** Injectable clock (unix seconds), for tests. */
|
|
177
|
+
readonly now?: () => number;
|
|
178
|
+
/**
|
|
179
|
+
* The relayer's submission queue. Pass the SAME queue to every component
|
|
180
|
+
* that broadcasts from this relayer key (the refund relay does), so their
|
|
181
|
+
* transactions never race for the account nonce. Default: a private queue.
|
|
182
|
+
*/
|
|
183
|
+
readonly relayerQueue?: RelayerQueue;
|
|
184
|
+
/**
|
|
185
|
+
* Advertise `refundRelay: true` in the `/supported` marker: this service
|
|
186
|
+
* also runs the gasless refund relay (`/streams/:streamId/...`). Set only
|
|
187
|
+
* when the relay is actually mounted. Default false.
|
|
188
|
+
*/
|
|
189
|
+
readonly advertiseRefundRelay?: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* One remembered EIP-3009 authorization, keyed by chain + asset + payer +
|
|
194
|
+
* authorization nonce.
|
|
195
|
+
*
|
|
196
|
+
* - `in-flight`: a settle is actively running (verify → submit → receipt).
|
|
197
|
+
* - `pending`: submitted, but the receipt wait timed out; the next settle of
|
|
198
|
+
* the SAME payload reconciles against `hash` instead of broadcasting again.
|
|
199
|
+
* - `final`: settled (the one success was delivered) or otherwise terminal.
|
|
200
|
+
*/
|
|
201
|
+
type LedgerEntry =
|
|
202
|
+
| {
|
|
203
|
+
readonly state: "in-flight";
|
|
204
|
+
readonly validBefore: bigint;
|
|
205
|
+
hash?: Hex;
|
|
206
|
+
}
|
|
207
|
+
| {
|
|
208
|
+
readonly state: "pending";
|
|
209
|
+
readonly validBefore: bigint;
|
|
210
|
+
readonly hash: Hex;
|
|
211
|
+
readonly payload: PaymentPayload;
|
|
212
|
+
readonly requirements: PaymentRequirements;
|
|
213
|
+
readonly auth: ExactEIP3009Payload & { signature: Hex };
|
|
214
|
+
readonly terms: OpenerTerms;
|
|
215
|
+
readonly opener: Address;
|
|
216
|
+
}
|
|
217
|
+
| {
|
|
218
|
+
readonly state: "final";
|
|
219
|
+
readonly validBefore: bigint;
|
|
220
|
+
readonly hash: Hex | "";
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** What {@link MetrikOpenerExactScheme.init} proved about the on-chain wiring. */
|
|
224
|
+
export interface OpenerFacilitatorBinding {
|
|
225
|
+
readonly chainId: number;
|
|
226
|
+
readonly factory: Address;
|
|
227
|
+
readonly escrow: Address;
|
|
228
|
+
readonly usdc: Address;
|
|
229
|
+
readonly relayer: Address;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Marker this facilitator advertises in `GET /supported` (`kinds[].extra`), so
|
|
234
|
+
* a resource server can refuse to quote an opener `payTo` through any
|
|
235
|
+
* facilitator that would settle it with a plain `transferWithAuthorization`
|
|
236
|
+
* (which strands the funds). See {@link readOpenerFacilitatorMarker}.
|
|
237
|
+
*/
|
|
238
|
+
export interface OpenerFacilitatorMarker {
|
|
239
|
+
readonly settlement: "x402-stream-opener";
|
|
240
|
+
readonly factory: Address;
|
|
241
|
+
readonly escrow: Address;
|
|
242
|
+
readonly usdc: Address;
|
|
243
|
+
readonly chainId: number;
|
|
244
|
+
/** Smallest accepted amount, base units (decimal string). */
|
|
245
|
+
readonly minAmount: string;
|
|
246
|
+
/** How long `settle` waits for a receipt before `settlement_pending`. */
|
|
247
|
+
readonly receiptTimeoutSeconds: number;
|
|
248
|
+
/**
|
|
249
|
+
* True when the same service runs the gasless refund relay
|
|
250
|
+
* (`GET /streams/:streamId`, `POST /streams/:streamId/close|reclaim`).
|
|
251
|
+
* Absent (read as false) otherwise.
|
|
252
|
+
*/
|
|
253
|
+
readonly refundRelay?: true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
|
|
257
|
+
readonly scheme = OPENER_SCHEME;
|
|
258
|
+
readonly caipFamily = "eip155:*";
|
|
259
|
+
|
|
260
|
+
private readonly stock: ExactEvmScheme;
|
|
261
|
+
private readonly chainId: number;
|
|
262
|
+
private readonly allowlist: ReadonlySet<string>;
|
|
263
|
+
private readonly receiptTimeoutMs: number;
|
|
264
|
+
private readonly minAmount: bigint;
|
|
265
|
+
private readonly now: () => number;
|
|
266
|
+
private readonly ledger = new Map<string, LedgerEntry>();
|
|
267
|
+
/** Serializes relayer submissions (simulate + write) so nonces never collide. */
|
|
268
|
+
private readonly relayerQueue: RelayerQueue;
|
|
269
|
+
private binding: OpenerFacilitatorBinding | undefined;
|
|
270
|
+
|
|
271
|
+
constructor(private readonly config: MetrikOpenerExactSchemeConfig) {
|
|
272
|
+
this.chainId = chainIdFromNetwork(config.network);
|
|
273
|
+
if (!isAddress(config.factoryAddress)) {
|
|
274
|
+
throw new Error("factoryAddress must be an EVM address");
|
|
275
|
+
}
|
|
276
|
+
if (!isAddress(config.escrowAddress)) {
|
|
277
|
+
throw new Error("escrowAddress must be an EVM address");
|
|
278
|
+
}
|
|
279
|
+
if (config.assetAllowlist.length === 0) {
|
|
280
|
+
throw new Error("assetAllowlist must not be empty");
|
|
281
|
+
}
|
|
282
|
+
for (const asset of config.assetAllowlist) {
|
|
283
|
+
if (!isAddress(asset)) {
|
|
284
|
+
throw new Error(`assetAllowlist entry is not an address: ${asset}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
this.allowlist = new Set(
|
|
288
|
+
config.assetAllowlist.map((asset) => asset.toLowerCase()),
|
|
289
|
+
);
|
|
290
|
+
this.receiptTimeoutMs = config.receiptTimeoutMs ?? 60_000;
|
|
291
|
+
if (
|
|
292
|
+
!Number.isInteger(this.receiptTimeoutMs) ||
|
|
293
|
+
this.receiptTimeoutMs <= 0
|
|
294
|
+
) {
|
|
295
|
+
throw new Error("receiptTimeoutMs must be a positive integer");
|
|
296
|
+
}
|
|
297
|
+
this.minAmount = config.minAmount ?? DEFAULT_OPENER_MIN_AMOUNT;
|
|
298
|
+
if (this.minAmount <= 0n) {
|
|
299
|
+
throw new Error("minAmount must be > 0");
|
|
300
|
+
}
|
|
301
|
+
this.now = config.now ?? (() => Math.floor(Date.now() / 1000));
|
|
302
|
+
this.relayerQueue = config.relayerQueue ?? new RelayerQueue();
|
|
303
|
+
|
|
304
|
+
// The stock verifier gets a READ-ONLY signer. Its settle path would send the
|
|
305
|
+
// USDC straight to payTo; making writes impossible closes that path even if
|
|
306
|
+
// a future refactor called it by mistake.
|
|
307
|
+
const publicClient = config.publicClient;
|
|
308
|
+
const refuseWrite = async (): Promise<never> => {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"MetrikOpenerExactScheme: the stock exact scheme may not broadcast; opener payments settle only via deployAndSettle",
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
this.stock = new ExactEvmScheme(
|
|
314
|
+
toFacilitatorEvmSigner({
|
|
315
|
+
address: config.walletClient.account.address,
|
|
316
|
+
readContract: (args) =>
|
|
317
|
+
publicClient.readContract(
|
|
318
|
+
args as Parameters<typeof publicClient.readContract>[0],
|
|
319
|
+
),
|
|
320
|
+
verifyTypedData: (args) =>
|
|
321
|
+
publicClient.verifyTypedData(
|
|
322
|
+
args as Parameters<typeof publicClient.verifyTypedData>[0],
|
|
323
|
+
),
|
|
324
|
+
getCode: (args) => publicClient.getCode(args),
|
|
325
|
+
waitForTransactionReceipt: refuseWrite,
|
|
326
|
+
writeContract: refuseWrite,
|
|
327
|
+
sendTransaction: refuseWrite,
|
|
328
|
+
}),
|
|
329
|
+
// ERC-6492 factory deployment disabled: 6492 signatures are refused
|
|
330
|
+
// before the stock verifier is ever reached.
|
|
331
|
+
{ eip6492AllowedFactories: [] },
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Proves the on-chain wiring before the facilitator serves anything: the RPC
|
|
337
|
+
* is on the configured chain, the factory has code, points at the configured
|
|
338
|
+
* escrow, and its token is allowlisted. Throws otherwise (the service refuses
|
|
339
|
+
* to start).
|
|
340
|
+
*/
|
|
341
|
+
async init(): Promise<OpenerFacilitatorBinding> {
|
|
342
|
+
const { publicClient, factoryAddress, escrowAddress } = this.config;
|
|
343
|
+
const rpcChainId = await publicClient.getChainId();
|
|
344
|
+
if (rpcChainId !== this.chainId) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`RPC chain id ${rpcChainId} does not match network ${this.config.network}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const code = await publicClient.getCode({ address: factoryAddress });
|
|
350
|
+
if (code === undefined || code === "0x") {
|
|
351
|
+
throw new Error(`no contract at factory address ${factoryAddress}`);
|
|
352
|
+
}
|
|
353
|
+
const [factoryEscrow, factoryUsdc] = await Promise.all([
|
|
354
|
+
publicClient.readContract({
|
|
355
|
+
address: factoryAddress,
|
|
356
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
357
|
+
functionName: "escrow",
|
|
358
|
+
}),
|
|
359
|
+
publicClient.readContract({
|
|
360
|
+
address: factoryAddress,
|
|
361
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
362
|
+
functionName: "usdc",
|
|
363
|
+
}),
|
|
364
|
+
]);
|
|
365
|
+
if (getAddress(factoryEscrow) !== getAddress(escrowAddress)) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`factory escrow ${factoryEscrow} is not the configured escrow ${escrowAddress}`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (!this.allowlist.has(factoryUsdc.toLowerCase())) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`factory settlement token ${factoryUsdc} is not on the asset allowlist`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
this.binding = {
|
|
376
|
+
chainId: this.chainId,
|
|
377
|
+
factory: getAddress(factoryAddress),
|
|
378
|
+
escrow: getAddress(factoryEscrow),
|
|
379
|
+
usdc: getAddress(factoryUsdc),
|
|
380
|
+
relayer: this.config.walletClient.account.address,
|
|
381
|
+
};
|
|
382
|
+
return this.binding;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** `/supported` extra: the marker a resource server checks before quoting an opener. */
|
|
386
|
+
getExtra(_network: string): Record<string, unknown> | undefined {
|
|
387
|
+
const binding = this.binding;
|
|
388
|
+
if (binding === undefined) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
const marker: OpenerFacilitatorMarker = {
|
|
392
|
+
settlement: "x402-stream-opener",
|
|
393
|
+
factory: binding.factory,
|
|
394
|
+
escrow: binding.escrow,
|
|
395
|
+
usdc: binding.usdc,
|
|
396
|
+
chainId: binding.chainId,
|
|
397
|
+
minAmount: this.minAmount.toString(),
|
|
398
|
+
receiptTimeoutSeconds: Math.ceil(this.receiptTimeoutMs / 1000),
|
|
399
|
+
...(this.config.advertiseRefundRelay === true
|
|
400
|
+
? { refundRelay: true as const }
|
|
401
|
+
: {}),
|
|
402
|
+
};
|
|
403
|
+
return { [OPENER_EXTRA_KEY]: marker };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getSigners(_network: string): string[] {
|
|
407
|
+
return [this.config.walletClient.account.address];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async verify(
|
|
411
|
+
payload: PaymentPayload,
|
|
412
|
+
requirements: PaymentRequirements,
|
|
413
|
+
context?: FacilitatorContext,
|
|
414
|
+
): Promise<VerifyResponse> {
|
|
415
|
+
try {
|
|
416
|
+
const key = this.ledgerKey(payload);
|
|
417
|
+
const entry = key === undefined ? undefined : this.ledger.get(key);
|
|
418
|
+
if (entry !== undefined) {
|
|
419
|
+
if (
|
|
420
|
+
entry.state === "pending" &&
|
|
421
|
+
deepEqual(entry.payload, payload) &&
|
|
422
|
+
deepEqual(entry.requirements, requirements)
|
|
423
|
+
) {
|
|
424
|
+
// Submitted earlier, receipt not yet seen: the authorization may
|
|
425
|
+
// already be used on-chain, so the on-chain checks would refuse it.
|
|
426
|
+
// The retry is valid only so that settle can reconcile THIS tx.
|
|
427
|
+
return { isValid: true, payer: entry.auth.authorization.from };
|
|
428
|
+
}
|
|
429
|
+
return invalid(
|
|
430
|
+
OpenerInvalidReason.duplicateSettlement,
|
|
431
|
+
"this authorization is already being settled or was settled",
|
|
432
|
+
payerOf(payload),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const checked = await this.check(payload, requirements, context);
|
|
436
|
+
if (!checked.ok) {
|
|
437
|
+
return checked.response;
|
|
438
|
+
}
|
|
439
|
+
return { isValid: true, payer: checked.auth.authorization.from };
|
|
440
|
+
} catch (error) {
|
|
441
|
+
return invalid(
|
|
442
|
+
OpenerInvalidReason.verifyError,
|
|
443
|
+
describe(error),
|
|
444
|
+
payerOf(payload),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async settle(
|
|
450
|
+
payload: PaymentPayload,
|
|
451
|
+
requirements: PaymentRequirements,
|
|
452
|
+
context?: FacilitatorContext,
|
|
453
|
+
): Promise<SettleResponse> {
|
|
454
|
+
const network = requirements.network;
|
|
455
|
+
const payer = payerOf(payload);
|
|
456
|
+
const key = this.ledgerKey(payload);
|
|
457
|
+
if (key === undefined) {
|
|
458
|
+
// Not a parseable EIP-3009 authorization: the full check refuses it.
|
|
459
|
+
const checked = await this.check(payload, requirements, context).catch(
|
|
460
|
+
(error: unknown) => ({
|
|
461
|
+
ok: false as const,
|
|
462
|
+
response: invalid(
|
|
463
|
+
OpenerInvalidReason.verifyError,
|
|
464
|
+
describe(error),
|
|
465
|
+
payer,
|
|
466
|
+
),
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
return checked.ok
|
|
470
|
+
? failure(
|
|
471
|
+
OpenerInvalidReason.verifyError,
|
|
472
|
+
network,
|
|
473
|
+
payer,
|
|
474
|
+
"unparseable authorization",
|
|
475
|
+
)
|
|
476
|
+
: refusal(checked.response, network, payer);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── At most one success per authorization ─────────────────────────────
|
|
480
|
+
// Everything from here to the ledger write is synchronous, so two
|
|
481
|
+
// concurrent settles of the same authorization can never both claim it.
|
|
482
|
+
this.pruneLedger();
|
|
483
|
+
const existing = this.ledger.get(key);
|
|
484
|
+
if (existing !== undefined) {
|
|
485
|
+
if (
|
|
486
|
+
existing.state === "pending" &&
|
|
487
|
+
deepEqual(existing.payload, payload) &&
|
|
488
|
+
deepEqual(existing.requirements, requirements)
|
|
489
|
+
) {
|
|
490
|
+
this.ledger.set(key, {
|
|
491
|
+
state: "in-flight",
|
|
492
|
+
validBefore: existing.validBefore,
|
|
493
|
+
hash: existing.hash,
|
|
494
|
+
});
|
|
495
|
+
return this.finish(
|
|
496
|
+
key,
|
|
497
|
+
payload,
|
|
498
|
+
requirements,
|
|
499
|
+
existing.validBefore,
|
|
500
|
+
await this.awaitSettlement(
|
|
501
|
+
existing.hash,
|
|
502
|
+
existing.auth,
|
|
503
|
+
existing.terms,
|
|
504
|
+
existing.opener,
|
|
505
|
+
network,
|
|
506
|
+
),
|
|
507
|
+
existing,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return failure(
|
|
511
|
+
OpenerInvalidReason.duplicateSettlement,
|
|
512
|
+
network,
|
|
513
|
+
payer,
|
|
514
|
+
"this authorization is already being settled or was settled",
|
|
515
|
+
existing.hash ?? "",
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (this.ledger.size >= MAX_LEDGER_ENTRIES) {
|
|
519
|
+
return failure(
|
|
520
|
+
OpenerInvalidReason.settlementCapacity,
|
|
521
|
+
network,
|
|
522
|
+
payer,
|
|
523
|
+
undefined,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const validBefore = BigInt(
|
|
527
|
+
(eip3009(payload) as ExactEIP3009Payload).authorization.validBefore,
|
|
528
|
+
);
|
|
529
|
+
const entry: LedgerEntry = { state: "in-flight", validBefore };
|
|
530
|
+
this.ledger.set(key, entry);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const checked = await this.check(payload, requirements, context);
|
|
534
|
+
if (!checked.ok) {
|
|
535
|
+
this.ledger.delete(key);
|
|
536
|
+
return refusal(checked.response, network, payer);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const { auth, terms, opener } = checked;
|
|
540
|
+
let hash: Hex;
|
|
541
|
+
try {
|
|
542
|
+
hash = await this.submitSerialized(auth, terms);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.ledger.delete(key);
|
|
545
|
+
return failure(
|
|
546
|
+
OpenerInvalidReason.simulationFailed,
|
|
547
|
+
network,
|
|
548
|
+
auth.authorization.from,
|
|
549
|
+
revertReason(error),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
entry.hash = hash;
|
|
553
|
+
return this.finish(
|
|
554
|
+
key,
|
|
555
|
+
payload,
|
|
556
|
+
requirements,
|
|
557
|
+
validBefore,
|
|
558
|
+
await this.awaitSettlement(hash, auth, terms, opener, network),
|
|
559
|
+
{ auth, terms, opener },
|
|
560
|
+
);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
// Unknown state after a possible broadcast: keep the authorization
|
|
563
|
+
// blocked (never a second success), but only if something was sent.
|
|
564
|
+
if (entry.hash === undefined) {
|
|
565
|
+
this.ledger.delete(key);
|
|
566
|
+
} else {
|
|
567
|
+
this.ledger.set(key, { state: "final", validBefore, hash: entry.hash });
|
|
568
|
+
}
|
|
569
|
+
return failure(
|
|
570
|
+
OpenerInvalidReason.settleError,
|
|
571
|
+
network,
|
|
572
|
+
payer,
|
|
573
|
+
describe(error),
|
|
574
|
+
entry.hash ?? "",
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Records the outcome of one settle attempt in the ledger and returns it. */
|
|
580
|
+
private finish(
|
|
581
|
+
key: string,
|
|
582
|
+
payload: PaymentPayload,
|
|
583
|
+
requirements: PaymentRequirements,
|
|
584
|
+
validBefore: bigint,
|
|
585
|
+
result: SettleResponse,
|
|
586
|
+
context: {
|
|
587
|
+
readonly auth: ExactEIP3009Payload & { signature: Hex };
|
|
588
|
+
readonly terms: OpenerTerms;
|
|
589
|
+
readonly opener: Address;
|
|
590
|
+
},
|
|
591
|
+
): SettleResponse {
|
|
592
|
+
if (result.success) {
|
|
593
|
+
// THE one success for this authorization. Nothing succeeds again.
|
|
594
|
+
this.ledger.set(key, {
|
|
595
|
+
state: "final",
|
|
596
|
+
validBefore,
|
|
597
|
+
hash: result.transaction as Hex,
|
|
598
|
+
});
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
if (result.errorReason === OpenerInvalidReason.settlementPending) {
|
|
602
|
+
this.ledger.set(key, {
|
|
603
|
+
state: "pending",
|
|
604
|
+
validBefore,
|
|
605
|
+
hash: result.transaction as Hex,
|
|
606
|
+
payload,
|
|
607
|
+
requirements,
|
|
608
|
+
auth: context.auth,
|
|
609
|
+
terms: context.terms,
|
|
610
|
+
opener: context.opener,
|
|
611
|
+
});
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
if (result.errorReason === OpenerInvalidReason.transactionReverted) {
|
|
615
|
+
// A reverted deployAndSettle consumed nothing (the whole call reverts),
|
|
616
|
+
// so the authorization may be settled again.
|
|
617
|
+
this.ledger.delete(key);
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
// Mined but not the expected stream, or anything else terminal: block.
|
|
621
|
+
this.ledger.set(key, {
|
|
622
|
+
state: "final",
|
|
623
|
+
validBefore,
|
|
624
|
+
hash: (result.transaction as Hex) || "",
|
|
625
|
+
});
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Simulate + broadcast `deployAndSettle`, one at a time. Concurrent
|
|
631
|
+
* submissions from one relayer would otherwise race for the same account
|
|
632
|
+
* nonce and all but one would fail after the upstream already did the work.
|
|
633
|
+
*/
|
|
634
|
+
private submitSerialized(
|
|
635
|
+
auth: ExactEIP3009Payload & { signature: Hex },
|
|
636
|
+
terms: OpenerTerms,
|
|
637
|
+
): Promise<Hex> {
|
|
638
|
+
const run = async (): Promise<Hex> => {
|
|
639
|
+
const { request } = await this.config.publicClient.simulateContract({
|
|
640
|
+
account: this.config.walletClient.account,
|
|
641
|
+
address: this.config.factoryAddress,
|
|
642
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
643
|
+
functionName: "deployAndSettle",
|
|
644
|
+
args: [termsTuple(terms), authorizationTuple(auth)],
|
|
645
|
+
});
|
|
646
|
+
return this.config.walletClient.writeContract({
|
|
647
|
+
...request,
|
|
648
|
+
chain: this.config.walletClient.chain ?? null,
|
|
649
|
+
} as Parameters<typeof this.config.walletClient.writeContract>[0]);
|
|
650
|
+
};
|
|
651
|
+
return this.relayerQueue.run(run);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** chain + asset + payer + authorization nonce, or undefined if unparseable. */
|
|
655
|
+
private ledgerKey(payload: PaymentPayload): string | undefined {
|
|
656
|
+
const auth = eip3009(payload);
|
|
657
|
+
const asset = payload.accepted?.asset;
|
|
658
|
+
if (auth === undefined || typeof asset !== "string") return undefined;
|
|
659
|
+
return [
|
|
660
|
+
this.chainId,
|
|
661
|
+
asset.toLowerCase(),
|
|
662
|
+
auth.authorization.from.toLowerCase(),
|
|
663
|
+
auth.authorization.nonce.toLowerCase(),
|
|
664
|
+
].join(":");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Drops entries whose authorization can no longer be settled by anyone
|
|
669
|
+
* (past `validBefore` plus a grace). Never drops an in-flight entry.
|
|
670
|
+
*/
|
|
671
|
+
private pruneLedger(): void {
|
|
672
|
+
const now = BigInt(this.now());
|
|
673
|
+
for (const [key, entry] of this.ledger) {
|
|
674
|
+
if (
|
|
675
|
+
entry.state !== "in-flight" &&
|
|
676
|
+
entry.validBefore + LEDGER_GRACE_SECONDS < now
|
|
677
|
+
) {
|
|
678
|
+
this.ledger.delete(key);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private async awaitSettlement(
|
|
684
|
+
hash: Hex,
|
|
685
|
+
auth: ExactEIP3009Payload & { signature: Hex },
|
|
686
|
+
terms: OpenerTerms,
|
|
687
|
+
opener: Address,
|
|
688
|
+
network: Network,
|
|
689
|
+
): Promise<SettleResponse> {
|
|
690
|
+
const payer = getAddress(auth.authorization.from);
|
|
691
|
+
let receipt;
|
|
692
|
+
try {
|
|
693
|
+
receipt = await this.config.publicClient.waitForTransactionReceipt({
|
|
694
|
+
hash,
|
|
695
|
+
timeout: this.receiptTimeoutMs,
|
|
696
|
+
});
|
|
697
|
+
} catch (error) {
|
|
698
|
+
// Broadcast but unconfirmed: keep the pending entry so a retry
|
|
699
|
+
// reconciles against this hash. Never success without a receipt.
|
|
700
|
+
return failure(
|
|
701
|
+
OpenerInvalidReason.settlementPending,
|
|
702
|
+
network,
|
|
703
|
+
payer,
|
|
704
|
+
describe(error),
|
|
705
|
+
hash,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (receipt.status !== "success") {
|
|
709
|
+
return failure(
|
|
710
|
+
OpenerInvalidReason.transactionReverted,
|
|
711
|
+
network,
|
|
712
|
+
payer,
|
|
713
|
+
"deployAndSettle reverted on-chain",
|
|
714
|
+
hash,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
const opened = findStreamOpened(
|
|
718
|
+
receipt.logs,
|
|
719
|
+
opener,
|
|
720
|
+
getAddress(this.config.escrowAddress),
|
|
721
|
+
auth,
|
|
722
|
+
terms,
|
|
723
|
+
);
|
|
724
|
+
if (opened === undefined) {
|
|
725
|
+
return failure(
|
|
726
|
+
OpenerInvalidReason.streamEventMismatch,
|
|
727
|
+
network,
|
|
728
|
+
payer,
|
|
729
|
+
"the mined receipt does not show the expected stream",
|
|
730
|
+
hash,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
transaction: hash,
|
|
736
|
+
network,
|
|
737
|
+
payer,
|
|
738
|
+
amount: auth.authorization.value,
|
|
739
|
+
extra: {
|
|
740
|
+
[OPENER_EXTRA_KEY]: {
|
|
741
|
+
streamId: opened.streamId,
|
|
742
|
+
opener,
|
|
743
|
+
escrow: getAddress(this.config.escrowAddress),
|
|
744
|
+
deposit: auth.authorization.value,
|
|
745
|
+
ratePerSecond: terms.ratePerSecond.toString(),
|
|
746
|
+
// A JSON NUMBER: the SDK buyer (payWithX402) validates it as one.
|
|
747
|
+
expiresAt: Number(opened.expiresAt),
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Every Metrik check plus the stock verifier. Cheapest, most local first. */
|
|
754
|
+
private async check(
|
|
755
|
+
payload: PaymentPayload,
|
|
756
|
+
requirements: PaymentRequirements,
|
|
757
|
+
context: FacilitatorContext | undefined,
|
|
758
|
+
): Promise<
|
|
759
|
+
| {
|
|
760
|
+
readonly ok: true;
|
|
761
|
+
readonly auth: ExactEIP3009Payload & { signature: Hex };
|
|
762
|
+
readonly terms: OpenerTerms;
|
|
763
|
+
readonly opener: Address;
|
|
764
|
+
}
|
|
765
|
+
| { readonly ok: false; readonly response: VerifyResponse }
|
|
766
|
+
> {
|
|
767
|
+
const payer = payerOf(payload);
|
|
768
|
+
const refuse = (reason: string, message?: string) =>
|
|
769
|
+
({ ok: false, response: invalid(reason, message, payer) }) as const;
|
|
770
|
+
|
|
771
|
+
if (payload.x402Version !== 2) {
|
|
772
|
+
return refuse(OpenerInvalidReason.unsupportedVersion);
|
|
773
|
+
}
|
|
774
|
+
if (requirements.scheme !== OPENER_SCHEME) {
|
|
775
|
+
return refuse(OpenerInvalidReason.unsupportedScheme);
|
|
776
|
+
}
|
|
777
|
+
if (
|
|
778
|
+
requirements.network !== this.config.network ||
|
|
779
|
+
payload.accepted?.network !== this.config.network
|
|
780
|
+
) {
|
|
781
|
+
return refuse(OpenerInvalidReason.unsupportedNetwork);
|
|
782
|
+
}
|
|
783
|
+
// The payer signed against `accepted`; settle exactly that, nothing else.
|
|
784
|
+
if (!deepEqual(payload.accepted, requirements)) {
|
|
785
|
+
return refuse(OpenerInvalidReason.acceptedMismatch);
|
|
786
|
+
}
|
|
787
|
+
const method = requirements.extra?.assetTransferMethod;
|
|
788
|
+
if (method !== undefined && method !== "eip3009") {
|
|
789
|
+
return refuse(OpenerInvalidReason.unsupportedAssetTransferMethod);
|
|
790
|
+
}
|
|
791
|
+
const auth = eip3009(payload);
|
|
792
|
+
if (auth === undefined) {
|
|
793
|
+
return refuse(OpenerInvalidReason.unsupportedAssetTransferMethod);
|
|
794
|
+
}
|
|
795
|
+
if (auth.signature === undefined || !isHex(auth.signature)) {
|
|
796
|
+
return refuse(OpenerInvalidReason.missingSignature);
|
|
797
|
+
}
|
|
798
|
+
if (isErc6492Signature(auth.signature)) {
|
|
799
|
+
return refuse(
|
|
800
|
+
OpenerInvalidReason.erc6492Unsupported,
|
|
801
|
+
"ERC-6492 (counterfactual smart wallet) signatures cannot be settled through the stream opener; deploy the wallet first",
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const binding = this.binding;
|
|
805
|
+
if (binding === undefined) {
|
|
806
|
+
return refuse(
|
|
807
|
+
OpenerInvalidReason.verifyError,
|
|
808
|
+
"facilitator not initialized",
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (
|
|
812
|
+
!isAddress(requirements.asset) ||
|
|
813
|
+
!this.allowlist.has(requirements.asset.toLowerCase()) ||
|
|
814
|
+
getAddress(requirements.asset) !== binding.usdc
|
|
815
|
+
) {
|
|
816
|
+
return refuse(OpenerInvalidReason.assetNotAllowed);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let terms: OpenerTerms;
|
|
820
|
+
let factory: Address;
|
|
821
|
+
try {
|
|
822
|
+
({ terms, factory } = termsFromExtra(requirements.extra));
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return refuse(OpenerInvalidReason.invalidTerms, describe(error));
|
|
825
|
+
}
|
|
826
|
+
if (factory !== binding.factory) {
|
|
827
|
+
return refuse(OpenerInvalidReason.factoryMismatch);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
let amount: bigint;
|
|
831
|
+
try {
|
|
832
|
+
amount = parseUintString(requirements.amount, "amount");
|
|
833
|
+
} catch (error) {
|
|
834
|
+
return refuse(OpenerInvalidReason.invalidAmount, describe(error));
|
|
835
|
+
}
|
|
836
|
+
if (amount <= 0n) {
|
|
837
|
+
return refuse(OpenerInvalidReason.invalidAmount, "amount must be > 0");
|
|
838
|
+
}
|
|
839
|
+
if (amount < this.minAmount) {
|
|
840
|
+
return refuse(
|
|
841
|
+
OpenerInvalidReason.amountBelowMinimum,
|
|
842
|
+
`amount ${amount} is below this facilitator's minimum ${this.minAmount}`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (amount > maxEarnable(terms)) {
|
|
846
|
+
return refuse(OpenerInvalidReason.amountExceedsTerms);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const predicted = await this.config.publicClient.readContract({
|
|
850
|
+
address: binding.factory,
|
|
851
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
852
|
+
functionName: "predict",
|
|
853
|
+
args: [termsTuple(terms)],
|
|
854
|
+
});
|
|
855
|
+
if (
|
|
856
|
+
!isAddress(requirements.payTo) ||
|
|
857
|
+
getAddress(requirements.payTo) !== getAddress(predicted)
|
|
858
|
+
) {
|
|
859
|
+
return refuse(OpenerInvalidReason.payToMismatch);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const listing = await this.config.listingResolver.resolve(terms);
|
|
863
|
+
if (!listing.ok) {
|
|
864
|
+
const reason = {
|
|
865
|
+
"listing-registry-unavailable": OpenerInvalidReason.listingUnavailable,
|
|
866
|
+
"listing-not-found": OpenerInvalidReason.listingNotFound,
|
|
867
|
+
"listing-ambiguous": OpenerInvalidReason.listingAmbiguous,
|
|
868
|
+
"listing-rate-out-of-bounds":
|
|
869
|
+
OpenerInvalidReason.listingRateOutOfBounds,
|
|
870
|
+
}[listing.reason];
|
|
871
|
+
return refuse(reason, listing.message);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Standard exact checks: signature, to == payTo, value == amount, window,
|
|
875
|
+
// asset contract, and a simulation of the EIP-3009 transfer itself.
|
|
876
|
+
const stock = await this.stock.verify(payload, requirements, context);
|
|
877
|
+
if (!stock.isValid) {
|
|
878
|
+
return { ok: false, response: stock };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const signed = auth as ExactEIP3009Payload & { signature: Hex };
|
|
882
|
+
try {
|
|
883
|
+
await this.config.publicClient.simulateContract({
|
|
884
|
+
account: this.config.walletClient.account,
|
|
885
|
+
address: binding.factory,
|
|
886
|
+
abi: x402StreamOpenerFactoryAbi,
|
|
887
|
+
functionName: "deployAndSettle",
|
|
888
|
+
args: [termsTuple(terms), authorizationTuple(signed)],
|
|
889
|
+
});
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return refuse(OpenerInvalidReason.simulationFailed, revertReason(error));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return { ok: true, auth: signed, terms, opener: getAddress(predicted) };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function authorizationTuple(auth: ExactEIP3009Payload & { signature: Hex }): {
|
|
899
|
+
readonly from: Address;
|
|
900
|
+
readonly value: bigint;
|
|
901
|
+
readonly validAfter: bigint;
|
|
902
|
+
readonly validBefore: bigint;
|
|
903
|
+
readonly nonce: Hex;
|
|
904
|
+
readonly signature: Hex;
|
|
905
|
+
} {
|
|
906
|
+
return {
|
|
907
|
+
from: getAddress(auth.authorization.from),
|
|
908
|
+
value: BigInt(auth.authorization.value),
|
|
909
|
+
validAfter: BigInt(auth.authorization.validAfter),
|
|
910
|
+
validBefore: BigInt(auth.authorization.validBefore),
|
|
911
|
+
nonce: auth.authorization.nonce,
|
|
912
|
+
signature: auth.signature,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Finds the pair of events a genuine opener settlement emits in ONE receipt:
|
|
918
|
+
* the escrow's `StreamOpened` with `buyer == opener` and exactly these terms,
|
|
919
|
+
* and the opener's `StreamOpenedFor` binding that stream to this payer, nonce
|
|
920
|
+
* and amount.
|
|
921
|
+
*/
|
|
922
|
+
function findStreamOpened(
|
|
923
|
+
logs: readonly Log[],
|
|
924
|
+
opener: Address,
|
|
925
|
+
escrow: Address,
|
|
926
|
+
auth: ExactEIP3009Payload,
|
|
927
|
+
terms: OpenerTerms,
|
|
928
|
+
): { readonly streamId: Hex; readonly expiresAt: bigint } | undefined {
|
|
929
|
+
const value = BigInt(auth.authorization.value);
|
|
930
|
+
const payer = getAddress(auth.authorization.from);
|
|
931
|
+
const escrowStreams = new Map<string, bigint>();
|
|
932
|
+
for (const log of logs) {
|
|
933
|
+
if (getAddress(log.address) !== escrow) continue;
|
|
934
|
+
try {
|
|
935
|
+
const decoded = decodeEventLog({
|
|
936
|
+
abi: streamEscrowV2Abi,
|
|
937
|
+
data: log.data,
|
|
938
|
+
topics: log.topics,
|
|
939
|
+
});
|
|
940
|
+
if (decoded.eventName !== "StreamOpened") continue;
|
|
941
|
+
const args = decoded.args as unknown as {
|
|
942
|
+
streamId: Hex;
|
|
943
|
+
buyer: Address;
|
|
944
|
+
operator: Address;
|
|
945
|
+
serviceRef: Hex;
|
|
946
|
+
deposit: bigint;
|
|
947
|
+
ratePerSecond: bigint;
|
|
948
|
+
expiresAt: bigint;
|
|
949
|
+
};
|
|
950
|
+
if (
|
|
951
|
+
getAddress(args.buyer) === opener &&
|
|
952
|
+
getAddress(args.operator) === getAddress(terms.operator) &&
|
|
953
|
+
args.serviceRef.toLowerCase() === terms.serviceRef.toLowerCase() &&
|
|
954
|
+
args.deposit === value &&
|
|
955
|
+
args.ratePerSecond === terms.ratePerSecond
|
|
956
|
+
) {
|
|
957
|
+
escrowStreams.set(args.streamId.toLowerCase(), BigInt(args.expiresAt));
|
|
958
|
+
}
|
|
959
|
+
} catch {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
for (const log of logs) {
|
|
964
|
+
if (getAddress(log.address) !== opener) continue;
|
|
965
|
+
try {
|
|
966
|
+
const decoded = decodeEventLog({
|
|
967
|
+
abi: x402StreamOpenerAbi,
|
|
968
|
+
data: log.data,
|
|
969
|
+
topics: log.topics,
|
|
970
|
+
});
|
|
971
|
+
if (decoded.eventName !== "StreamOpenedFor") continue;
|
|
972
|
+
const args = decoded.args;
|
|
973
|
+
const expiresAt = escrowStreams.get(args.streamId.toLowerCase());
|
|
974
|
+
if (
|
|
975
|
+
expiresAt !== undefined &&
|
|
976
|
+
getAddress(args.payer) === payer &&
|
|
977
|
+
args.authorizationNonce.toLowerCase() ===
|
|
978
|
+
auth.authorization.nonce.toLowerCase() &&
|
|
979
|
+
args.deposit === value
|
|
980
|
+
) {
|
|
981
|
+
return { streamId: args.streamId, expiresAt };
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Reads an opener facilitator marker out of a `/supported` kind's `extra`, or
|
|
992
|
+
* `undefined` when the facilitator does not advertise opener settlement.
|
|
993
|
+
*/
|
|
994
|
+
export function readOpenerFacilitatorMarker(
|
|
995
|
+
extra: Record<string, unknown> | undefined,
|
|
996
|
+
): OpenerFacilitatorMarker | undefined {
|
|
997
|
+
const raw = extra?.[OPENER_EXTRA_KEY];
|
|
998
|
+
if (typeof raw !== "object" || raw === null) return undefined;
|
|
999
|
+
const marker = raw as Record<string, unknown>;
|
|
1000
|
+
if (
|
|
1001
|
+
marker.settlement !== "x402-stream-opener" ||
|
|
1002
|
+
typeof marker.factory !== "string" ||
|
|
1003
|
+
!isAddress(marker.factory) ||
|
|
1004
|
+
typeof marker.escrow !== "string" ||
|
|
1005
|
+
!isAddress(marker.escrow) ||
|
|
1006
|
+
typeof marker.usdc !== "string" ||
|
|
1007
|
+
!isAddress(marker.usdc) ||
|
|
1008
|
+
typeof marker.chainId !== "number" ||
|
|
1009
|
+
typeof marker.minAmount !== "string" ||
|
|
1010
|
+
!/^[1-9]\d*$/.test(marker.minAmount) ||
|
|
1011
|
+
typeof marker.receiptTimeoutSeconds !== "number" ||
|
|
1012
|
+
!Number.isInteger(marker.receiptTimeoutSeconds) ||
|
|
1013
|
+
marker.receiptTimeoutSeconds <= 0
|
|
1014
|
+
) {
|
|
1015
|
+
return undefined;
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
settlement: "x402-stream-opener",
|
|
1019
|
+
factory: getAddress(marker.factory),
|
|
1020
|
+
escrow: getAddress(marker.escrow),
|
|
1021
|
+
usdc: getAddress(marker.usdc),
|
|
1022
|
+
chainId: marker.chainId,
|
|
1023
|
+
minAmount: marker.minAmount,
|
|
1024
|
+
receiptTimeoutSeconds: marker.receiptTimeoutSeconds,
|
|
1025
|
+
...(marker.refundRelay === true ? { refundRelay: true as const } : {}),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function eip3009(payload: PaymentPayload): ExactEIP3009Payload | undefined {
|
|
1030
|
+
const inner = payload.payload as unknown;
|
|
1031
|
+
if (typeof inner !== "object" || inner === null) return undefined;
|
|
1032
|
+
const candidate = inner as ExactEvmPayloadV2;
|
|
1033
|
+
if (!isEIP3009Payload(candidate)) return undefined;
|
|
1034
|
+
const authorization = (candidate as ExactEIP3009Payload).authorization;
|
|
1035
|
+
if (
|
|
1036
|
+
typeof authorization !== "object" ||
|
|
1037
|
+
authorization === null ||
|
|
1038
|
+
typeof authorization.from !== "string" ||
|
|
1039
|
+
!isAddress(authorization.from) ||
|
|
1040
|
+
typeof authorization.to !== "string" ||
|
|
1041
|
+
typeof authorization.value !== "string" ||
|
|
1042
|
+
typeof authorization.validAfter !== "string" ||
|
|
1043
|
+
typeof authorization.validBefore !== "string" ||
|
|
1044
|
+
typeof authorization.nonce !== "string" ||
|
|
1045
|
+
!isHex(authorization.nonce) ||
|
|
1046
|
+
authorization.nonce.length !== 66
|
|
1047
|
+
) {
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
parseUintString(authorization.value, "authorization.value");
|
|
1052
|
+
parseUintString(authorization.validAfter, "authorization.validAfter");
|
|
1053
|
+
parseUintString(authorization.validBefore, "authorization.validBefore");
|
|
1054
|
+
} catch {
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
return candidate as ExactEIP3009Payload;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function payerOf(payload: PaymentPayload): string | undefined {
|
|
1061
|
+
const from = (
|
|
1062
|
+
payload.payload as { authorization?: { from?: unknown } } | undefined
|
|
1063
|
+
)?.authorization?.from;
|
|
1064
|
+
return typeof from === "string" && isAddress(from)
|
|
1065
|
+
? getAddress(from)
|
|
1066
|
+
: undefined;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function invalid(
|
|
1070
|
+
reason: string,
|
|
1071
|
+
message: string | undefined,
|
|
1072
|
+
payer: string | undefined,
|
|
1073
|
+
): VerifyResponse {
|
|
1074
|
+
return {
|
|
1075
|
+
isValid: false,
|
|
1076
|
+
invalidReason: reason,
|
|
1077
|
+
...(message === undefined ? {} : { invalidMessage: message }),
|
|
1078
|
+
...(payer === undefined ? {} : { payer }),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function failure(
|
|
1083
|
+
reason: string,
|
|
1084
|
+
network: Network,
|
|
1085
|
+
payer: string | undefined,
|
|
1086
|
+
message: string | undefined,
|
|
1087
|
+
transaction = "",
|
|
1088
|
+
): SettleResponse {
|
|
1089
|
+
return {
|
|
1090
|
+
success: false,
|
|
1091
|
+
errorReason: reason,
|
|
1092
|
+
...(message === undefined ? {} : { errorMessage: message }),
|
|
1093
|
+
transaction,
|
|
1094
|
+
network,
|
|
1095
|
+
...(payer === undefined ? {} : { payer }),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function refusal(
|
|
1100
|
+
response: VerifyResponse,
|
|
1101
|
+
network: Network,
|
|
1102
|
+
payer: string | undefined,
|
|
1103
|
+
): SettleResponse {
|
|
1104
|
+
return {
|
|
1105
|
+
success: false,
|
|
1106
|
+
errorReason: response.invalidReason ?? OpenerInvalidReason.verifyError,
|
|
1107
|
+
...(response.invalidMessage === undefined
|
|
1108
|
+
? {}
|
|
1109
|
+
: { errorMessage: response.invalidMessage }),
|
|
1110
|
+
transaction: "",
|
|
1111
|
+
network,
|
|
1112
|
+
...(payer === undefined ? {} : { payer }),
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function revertReason(error: unknown): string {
|
|
1117
|
+
if (error instanceof BaseError) {
|
|
1118
|
+
const reverted = error.walk(
|
|
1119
|
+
(cause) => cause instanceof ContractFunctionRevertedError,
|
|
1120
|
+
);
|
|
1121
|
+
if (reverted instanceof ContractFunctionRevertedError) {
|
|
1122
|
+
return (
|
|
1123
|
+
reverted.data?.errorName ?? reverted.reason ?? reverted.shortMessage
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return error.shortMessage;
|
|
1127
|
+
}
|
|
1128
|
+
return describe(error);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function describe(error: unknown): string {
|
|
1132
|
+
return error instanceof Error ? error.message : String(error);
|
|
1133
|
+
}
|