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