@metalntwx/metal-pay 0.2.0 → 0.3.0
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 +72 -33
- package/dist/clearing-abi.d.ts +1836 -0
- package/dist/clearing-abi.js +2361 -0
- package/dist/clearing-client.d.ts +49 -159
- package/dist/clearing-client.js +191 -564
- package/dist/clearing-codec.d.ts +57 -0
- package/dist/clearing-codec.js +173 -0
- package/dist/clearing-custody.d.ts +191 -0
- package/dist/clearing-custody.js +490 -0
- package/dist/clearing-server.d.ts +30 -49
- package/dist/clearing-server.js +87 -67
- package/dist/clearing-x402.d.ts +29 -0
- package/dist/clearing-x402.js +203 -0
- package/dist/client.d.ts +7 -13
- package/dist/client.js +16 -18
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mandates/binding.d.ts +4 -27
- package/dist/mandates/binding.js +5 -9
- package/dist/mandates/checkout.js +5 -5
- package/dist/mandates/client.js +4 -3
- package/dist/mandates/l3a.d.ts +2 -2
- package/dist/mandates/l3a.js +5 -4
- package/dist/mandates/seller.d.ts +1 -1
- package/dist/mandates/seller.js +1 -1
- package/dist/mandates/verify.d.ts +4 -6
- package/dist/mandates/verify.js +42 -66
- package/dist/mandates/vi/credential.js +4 -4
- package/dist/node.d.ts +11 -0
- package/dist/node.js +97 -0
- package/package.json +18 -11
- package/src/clearing-abi.ts +2361 -0
- package/src/clearing-client.ts +303 -0
- package/src/clearing-codec.test.ts +57 -0
- package/src/clearing-codec.ts +251 -0
- package/src/clearing-custody.test.ts +534 -0
- package/src/clearing-custody.ts +665 -0
- package/src/clearing-fixture.json +37 -0
- package/src/clearing-server.test.ts +388 -0
- package/src/clearing-server.ts +123 -0
- package/src/clearing-x402.test.ts +181 -0
- package/src/clearing-x402.ts +279 -0
- package/src/client.test.ts +325 -0
- package/src/client.ts +156 -0
- package/src/index.ts +33 -0
- package/src/mandates/account.ts +91 -0
- package/src/mandates/binding.ts +280 -0
- package/src/mandates/chain.ts +68 -0
- package/src/mandates/checkout.ts +368 -0
- package/src/mandates/client.ts +159 -0
- package/src/mandates/extension.ts +48 -0
- package/src/mandates/index.ts +121 -0
- package/src/mandates/ingest.ts +42 -0
- package/src/mandates/jcs.ts +16 -0
- package/src/mandates/l3a.ts +216 -0
- package/src/mandates/seller.ts +91 -0
- package/src/mandates/verify.ts +532 -0
- package/src/mandates/vi/credential.ts +477 -0
- package/src/mandates/vi/issue.ts +118 -0
- package/src/mandates/vi/sd-jwt.ts +189 -0
- package/src/networks.test.ts +16 -0
- package/src/networks.ts +152 -0
- package/src/node.test.ts +24 -0
- package/src/node.ts +96 -0
- package/src/server.test.ts +223 -0
- package/src/server.ts +77 -0
- package/dist/clearing.d.ts +0 -204
- package/dist/clearing.js +0 -151
- package/dist/file-storage.d.ts +0 -39
- package/dist/file-storage.js +0 -195
package/dist/mandates/binding.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MetalAccount mandate ABI, typed agent keys, references, self-calls and
|
|
1
|
+
// MetalAccount mandate ABI, typed agent keys, references, self-calls and pulls.
|
|
2
2
|
import { concat, encodeFunctionData, encodePacked, keccak256, parseAbi, size, slice } from "viem";
|
|
3
3
|
/** Domain tag for the payment-only ERC-1271 path on MetalAccount. */
|
|
4
4
|
export const MANDATE_SIGNATURE_TAG = "0x4d4e4454";
|
|
@@ -10,7 +10,6 @@ export const mandateBindingAbi = parseAbi([
|
|
|
10
10
|
"struct Terms { string kid; string instrument; string currency; uint256 unitsPerMinor; Payee[] payees; bytes32 openSdHash; }",
|
|
11
11
|
"struct PayeeTerms { bytes32 sdHash; string object; }",
|
|
12
12
|
"struct MandateState { address principal; bool revoked; bytes32 agentX; bytes32 agentY; string kid; bytes32 openSdHash; uint64 boundBlock; uint64 occurrences; uint128 spent; }",
|
|
13
|
-
"function mip20TransferHandler() pure returns (bytes4)",
|
|
14
13
|
"function mandateReference(address principal, bytes agentKey, bytes32 hashC) pure returns (bytes32)",
|
|
15
14
|
"function mandates(bytes32 ref) view returns (MandateState)",
|
|
16
15
|
"function payee(bytes32 ref, address account) view returns (PayeeTerms)",
|
|
@@ -21,9 +20,9 @@ export const mandateBindingAbi = parseAbi([
|
|
|
21
20
|
"function revoke(bytes32 ref)",
|
|
22
21
|
"function isValidSignature(bytes32 hash, bytes blob) view returns (bytes4)",
|
|
23
22
|
"function check(Pull p, L3a l) view returns (uint8)",
|
|
24
|
-
"function pull(Pull p, L3a l)",
|
|
23
|
+
"function pull(Pull p, L3a l) returns (bool paid)",
|
|
25
24
|
"event MandateBound(bytes32 indexed ref, bytes32 hashC, bytes32 issuerKey, bytes agentKey, Constraints constraints, Terms terms)",
|
|
26
|
-
"event MandatePayment(bytes32 indexed ref, address indexed to, bytes20 l3aDigest, bytes agentKey, uint256 amount, uint256 spent, uint256 occurrences
|
|
25
|
+
"event MandatePayment(bytes32 indexed ref, address indexed to, bytes20 l3aDigest, bytes agentKey, uint256 amount, uint256 spent, uint256 occurrences)",
|
|
27
26
|
"event MandateRefused(bytes32 indexed ref, address indexed to, bytes20 l3aDigest, bytes agentKey, uint256 amount, uint8 reason)",
|
|
28
27
|
"event MandateRevoked(bytes32 indexed ref)",
|
|
29
28
|
"event MandateReplaced(bytes32 indexed predecessor, bytes32 indexed successor)",
|
|
@@ -44,9 +43,6 @@ export const mandateBindingAbi = parseAbi([
|
|
|
44
43
|
"error NonceAlreadyUsed()",
|
|
45
44
|
"error NotYetValid()",
|
|
46
45
|
"error RefusalLimitReached()",
|
|
47
|
-
"error NotApproved()",
|
|
48
|
-
"error MandateBreach(uint8 reason)",
|
|
49
|
-
"error UnauthorizedHookCaller()",
|
|
50
46
|
]);
|
|
51
47
|
/** `MandateTypes.Reason` in declaration order, which is the order Mandate Binding checks them in; `None` is a pass. */
|
|
52
48
|
export const REASONS = [
|
|
@@ -74,13 +70,13 @@ export const REFUSALS = {
|
|
|
74
70
|
NoMandate: { reason: "mandate_not_bound", meaning: "no such mandate is bound on the account" },
|
|
75
71
|
Revoked: { reason: "mandate_revoked", meaning: "the mandate is revoked, or replaced: the agent pays under the credential that replaced it" },
|
|
76
72
|
Expired: { reason: "mandate_expired", meaning: "the mandate has expired" },
|
|
77
|
-
BadSignature: { reason: "mandate_bad_signature", meaning: "the payment is not signed by the mandate
|
|
73
|
+
BadSignature: { reason: "mandate_bad_signature", meaning: "the payment is not signed by the mandate key" },
|
|
78
74
|
NonceUsed: { reason: "mandate_nonce_used", meaning: "this payment was already made or refused" },
|
|
79
75
|
OutsideWindow: { reason: "mandate_outside_window", meaning: "the payment's validity window is not open" },
|
|
80
76
|
PayeeNotAllowed: { reason: "mandate_payee_not_listed", meaning: "the payee is not one the mandate allows" },
|
|
81
77
|
OverPerPaymentCap: { reason: "mandate_cap_exceeded", meaning: "the amount is above the mandate's per-payment cap" },
|
|
82
78
|
OverBudget: { reason: "mandate_budget_spent", meaning: "the amount would take the mandate past its budget" },
|
|
83
|
-
OutsideRecurrence: { reason: "mandate_outside_recurrence", meaning: "
|
|
79
|
+
OutsideRecurrence: { reason: "mandate_outside_recurrence", meaning: "the mandate has not started yet, or its dates have ended" },
|
|
84
80
|
OverOccurrences: { reason: "mandate_occurrences_used", meaning: "the mandate's payment count is used up" },
|
|
85
81
|
};
|
|
86
82
|
/** `mandate_cap_exceeded (the amount is above the mandate's per-payment cap)`: a refusal as a person reads it. */
|
|
@@ -61,7 +61,7 @@ export function readCheckoutJwt(jwt) {
|
|
|
61
61
|
const [h, p, sig] = segments;
|
|
62
62
|
const header = JSON.parse(Buffer.from(fromB64url(h)).toString("utf8"));
|
|
63
63
|
if (header.alg !== "ES256" || header.typ !== "JWT" || typeof header.kid !== "string")
|
|
64
|
-
throw new Error("a checkout JWT is an ES256 JWT naming the
|
|
64
|
+
throw new Error("a checkout JWT is an ES256 JWT naming the mandate key");
|
|
65
65
|
const offer = JSON.parse(Buffer.from(fromB64url(p)).toString("utf8"));
|
|
66
66
|
if (offer === null || typeof offer !== "object" || Array.isArray(offer))
|
|
67
67
|
throw new Error("a checkout JWT's payload is an object");
|
|
@@ -126,7 +126,7 @@ export function readL3b(serialized) {
|
|
|
126
126
|
const payload = JSON.parse(Buffer.from(fromB64url(p)).toString("utf8"));
|
|
127
127
|
const [, final] = JSON.parse(Buffer.from(fromB64url(checkoutDisclosure)).toString("utf8"));
|
|
128
128
|
if (typeof header.kid !== "string")
|
|
129
|
-
throw new Error("an L3b names the
|
|
129
|
+
throw new Error("an L3b names the mandate key");
|
|
130
130
|
if (final.vct !== CHECKOUT_VCT || typeof final.checkout_jwt !== "string")
|
|
131
131
|
throw new Error(`the disclosure is not a ${CHECKOUT_VCT} with a checkout JWT`);
|
|
132
132
|
const signature = fromB64url(sig);
|
|
@@ -183,9 +183,9 @@ function checkCheckoutMandate(serialized, expect) {
|
|
|
183
183
|
if (l3b.typ !== "kb-sd-jwt")
|
|
184
184
|
problems.push("the checkout mandate's typ is not kb-sd-jwt");
|
|
185
185
|
if (l3b.kid !== expect.kid)
|
|
186
|
-
problems.push("the checkout mandate names another
|
|
186
|
+
problems.push("the checkout mandate names another mandate key");
|
|
187
187
|
if (!verifyL3aSignature(expect.agent, sha256Hex(Buffer.from(l3b.signingInput, "ascii")), l3b.r, l3b.s))
|
|
188
|
-
problems.push("the checkout mandate is not signed by the mandate
|
|
188
|
+
problems.push("the checkout mandate is not signed by the mandate key");
|
|
189
189
|
if (typeof l3b.aud !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(l3b.aud) || !isAddressEqual(l3b.aud, expect.payee))
|
|
190
190
|
problems.push("the checkout mandate is for another payee");
|
|
191
191
|
const now = expect.now ?? Math.floor(Date.now() / 1000);
|
|
@@ -215,7 +215,7 @@ function checkCheckoutMandate(serialized, expect) {
|
|
|
215
215
|
let lineItemId = expect.lineItemId;
|
|
216
216
|
if (jwt !== undefined) {
|
|
217
217
|
if (jwt.kid !== expect.kid || !verifyL3aSignature(expect.agent, sha256Hex(Buffer.from(jwt.signingInput, "ascii")), jwt.r, jwt.s))
|
|
218
|
-
problems.push("the checkout JWT is not signed by the mandate
|
|
218
|
+
problems.push("the checkout JWT is not signed by the mandate key");
|
|
219
219
|
if (expect.offer !== undefined && jwt.signingInput !== buildCheckoutJwt(expect.kid, expect.offer).signingInput)
|
|
220
220
|
problems.push("the checkout JWT is not over the offer the payee served");
|
|
221
221
|
const offer = expect.offer ?? jwt.offer;
|
package/dist/mandates/client.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// `TransferWithAuthorization` whose `from` is the principal's account, whose
|
|
6
6
|
// nonce is `bytes12(mandate reference) ‖ bytes20(SHA-256(L3a signing input))`,
|
|
7
7
|
// whose window is the L3a's `iat` and `exp`, and whose signature is
|
|
8
|
-
// `abi.encode(Pull, L3a fields)`,
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// `MNDT || abi.encode(Pull, L3a fields)`, the arguments of `pull` on the
|
|
9
|
+
// principal's account. Sellers see an ordinary `exact` payment; Metal's
|
|
10
|
+
// facilitator submits it as that `pull`, where Mandate Binding accepts or
|
|
11
|
+
// refuses it. A seller that asks for the agent's signed order gets it in
|
|
11
12
|
// the payload's `extensions`, and the per-payment mandate names its hash.
|
|
12
13
|
//
|
|
13
14
|
// `@x402/evm`'s `exact` client draws its own nonce and signs EIP-712 as
|
package/dist/mandates/l3a.d.ts
CHANGED
|
@@ -64,9 +64,9 @@ export declare function signL3a(args: {
|
|
|
64
64
|
iss: string;
|
|
65
65
|
signDigest: SignDigest;
|
|
66
66
|
}): Promise<SignedL3a>;
|
|
67
|
-
/** The
|
|
67
|
+
/** The x402 payload's signature: `MNDT || abi.encode(Pull, L3a fields)`, the arguments of `pull`. */
|
|
68
68
|
export declare function encodeMandateBlob(pull: Pull, fields: L3aFields): Hex;
|
|
69
|
-
/** The Pull and L3a fields
|
|
69
|
+
/** The Pull and L3a fields an x402 payload's signature carries, as Mandate Binding's `check` and `pull` take them. */
|
|
70
70
|
export declare function decodeMandateBlob(blob: Hex): {
|
|
71
71
|
pull: Pull;
|
|
72
72
|
fields: L3aFields;
|
package/dist/mandates/l3a.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// verbatim), with `aud` the chain as `eip155:<chainId>`, `iat`/`exp` the
|
|
5
5
|
// ERC-3009 window, and `transaction_id` the SHA-256 of the RFC 8785 form of
|
|
6
6
|
// the seller's x402 payment requirements. The ERC-3009 nonce is
|
|
7
|
-
// `bytes12(mandate reference) ‖ bytes20(SHA-256(signing input))`; the
|
|
8
|
-
// signature is `MNDT || abi.encode(Pull, L3a fields)
|
|
7
|
+
// `bytes12(mandate reference) ‖ bytes20(SHA-256(signing input))`; the x402
|
|
8
|
+
// payload's signature is `MNDT || abi.encode(Pull, L3a fields)`, which the
|
|
9
|
+
// facilitator decodes into `pull`'s arguments.
|
|
9
10
|
import { p256 } from "@noble/curves/nist";
|
|
10
11
|
import { bytesToHex, concat, decodeAbiParameters, encodeAbiParameters, getAbiItem, hexToBigInt, hexToBytes, isAddressEqual, numberToHex, size, slice, zeroAddress, } from "viem";
|
|
11
12
|
import { mandateBindingAbi, referencePrefix } from "./binding.js";
|
|
@@ -145,11 +146,11 @@ export async function signL3a(args) {
|
|
|
145
146
|
return { pull, fields: { ...unsigned, r, s }, serialized: serializeL3a(l3a, r, s), digest: l3a.digest };
|
|
146
147
|
}
|
|
147
148
|
const pullInputs = getAbiItem({ abi: mandateBindingAbi, name: "pull" }).inputs;
|
|
148
|
-
/** The
|
|
149
|
+
/** The x402 payload's signature: `MNDT || abi.encode(Pull, L3a fields)`, the arguments of `pull`. */
|
|
149
150
|
export function encodeMandateBlob(pull, fields) {
|
|
150
151
|
return concat([MANDATE_SIGNATURE_TAG, encodeAbiParameters(pullInputs, [pull, fields])]);
|
|
151
152
|
}
|
|
152
|
-
/** The Pull and L3a fields
|
|
153
|
+
/** The Pull and L3a fields an x402 payload's signature carries, as Mandate Binding's `check` and `pull` take them. */
|
|
153
154
|
export function decodeMandateBlob(blob) {
|
|
154
155
|
if (size(blob) < 4 || slice(blob, 0, 4) !== MANDATE_SIGNATURE_TAG)
|
|
155
156
|
throw new Error("not a MetalAccount mandate signature");
|
|
@@ -17,7 +17,7 @@ export type RequireSignedOfferConfig = {
|
|
|
17
17
|
* On a route whose offer asks for it (`extra.signedOffer: "required"`), a
|
|
18
18
|
* payment under a mandate must carry the agent's signed order, checked before
|
|
19
19
|
* the facilitator is asked: the payer's account binds the mandate the payment
|
|
20
|
-
* names, and the signed order is that mandate's
|
|
20
|
+
* names, and the signed order is that mandate's key over the offer the
|
|
21
21
|
* server served, for this payee, buying its one line item (`extra.resource`
|
|
22
22
|
* when the route names one, else the payee). A refusal is a 402 whose reason
|
|
23
23
|
* names the check that failed, a payment that names no mandate included. A
|
package/dist/mandates/seller.js
CHANGED
|
@@ -13,7 +13,7 @@ import { asksSignedOffer, mandateRefOf, signedOfferOf } from "./extension.js";
|
|
|
13
13
|
* On a route whose offer asks for it (`extra.signedOffer: "required"`), a
|
|
14
14
|
* payment under a mandate must carry the agent's signed order, checked before
|
|
15
15
|
* the facilitator is asked: the payer's account binds the mandate the payment
|
|
16
|
-
* names, and the signed order is that mandate's
|
|
16
|
+
* names, and the signed order is that mandate's key over the offer the
|
|
17
17
|
* server served, for this payee, buying its one line item (`extra.resource`
|
|
18
18
|
* when the route names one, else the payee). A refusal is a 402 whose reason
|
|
19
19
|
* names the check that failed, a payment that names no mandate included. A
|
|
@@ -14,10 +14,10 @@ export interface VerifyStep {
|
|
|
14
14
|
detail: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const STEP_TITLES: {
|
|
17
|
-
readonly 1: "The mandate is bound on the principal's account: hash(C), the limits, the payees, the
|
|
18
|
-
readonly 2: "The credential in hand hashes to the bound hash(C) and states the bound limits, payees and
|
|
17
|
+
readonly 1: "The mandate is bound on the principal's account: hash(C), the limits, the payees, the mandate key and the issuer key are on chain";
|
|
18
|
+
readonly 2: "The credential in hand hashes to the bound hash(C) and states the bound limits, payees and mandate key";
|
|
19
19
|
readonly 3: "The credential's signatures: the issuer's under a trusted key that is the one recorded at binding, the owner's under the key the issuer bound";
|
|
20
|
-
readonly 4: "Each payment and refusal is the
|
|
20
|
+
readonly 4: "Each payment and refusal is the mandate key's per-payment mandate, rebuilt from its transaction, inside the mandate at its block";
|
|
21
21
|
};
|
|
22
22
|
/** A mandate as its `MandateBound` event records it. */
|
|
23
23
|
export interface BoundMandate {
|
|
@@ -40,11 +40,9 @@ export interface VerifiedPayment {
|
|
|
40
40
|
kind: "payment" | "refusal";
|
|
41
41
|
txHash: Hash;
|
|
42
42
|
block: bigint;
|
|
43
|
-
/** Whether `pull` submitted it, with no seller; otherwise a relayer submitted the ERC-3009 authorisation to the token. */
|
|
44
|
-
direct: boolean;
|
|
45
43
|
to: Address;
|
|
46
44
|
amount: bigint;
|
|
47
|
-
/** The ERC-3009 nonce the token recorded; a seller's `/settlements` row carries it for a
|
|
45
|
+
/** The ERC-3009 nonce the token recorded; a seller's `/settlements` row carries it for a payment the facilitator settled. */
|
|
48
46
|
nonce?: Hex;
|
|
49
47
|
reason?: ReasonName;
|
|
50
48
|
/** The L3a SD-JWT, as the VI reference serialises it, when the transaction carried one. */
|
package/dist/mandates/verify.js
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
// changes, so what it is now it was when it bound and paid. What a verifier
|
|
9
9
|
// cannot rebuild (the budget's running total, the nonce's single use) is read
|
|
10
10
|
// as the contract recorded it in the event and checked against the limits.
|
|
11
|
-
import { decodeFunctionData, formatLog, getAbiItem, hexToBigInt, isAddressEqual, numberToHex,
|
|
11
|
+
import { decodeFunctionData, formatLog, getAbiItem, hexToBigInt, isAddressEqual, numberToHex, erc20Abi, parseEventLogs, slice, toFunctionSelector, } from "viem";
|
|
12
12
|
import { REASONS, REFUSALS, decodeAgentKey, encodeAgentKey, mandateBindingAbi, mandateReference, reasonName, sameAgentKey, } from "./binding.js";
|
|
13
13
|
import { accountFactoryFor, metalNetwork } from "../networks.js";
|
|
14
14
|
import { assertMetalAccount, metalAccountCode, metalAccountFactoryAbi } from "./account.js";
|
|
15
15
|
import { publicMessage } from "./chain.js";
|
|
16
16
|
import { verifyCheckoutMandate, x402Offer, lineItemOf } from "./checkout.js";
|
|
17
17
|
import { jcs } from "./jcs.js";
|
|
18
|
-
import { buildL3a,
|
|
18
|
+
import { buildL3a, mandateNonce, serializeL3a, transactionIdOf, verifyL3aSignature } from "./l3a.js";
|
|
19
19
|
import { extractTerms, hashCredential, issuerKeyId, verifyCredential } from "./vi/credential.js";
|
|
20
20
|
function before(a, b) {
|
|
21
21
|
return a.block < b.block || (a.block === b.block && (a.transactionIndex < b.transactionIndex || (a.transactionIndex === b.transactionIndex && a.logIndex < b.logIndex)));
|
|
@@ -26,17 +26,11 @@ function position(log) {
|
|
|
26
26
|
return { block: log.blockNumber, transactionIndex: log.transactionIndex, logIndex: log.logIndex };
|
|
27
27
|
}
|
|
28
28
|
export const STEP_TITLES = {
|
|
29
|
-
1: "The mandate is bound on the principal's account: hash(C), the limits, the payees, the
|
|
30
|
-
2: "The credential in hand hashes to the bound hash(C) and states the bound limits, payees and
|
|
29
|
+
1: "The mandate is bound on the principal's account: hash(C), the limits, the payees, the mandate key and the issuer key are on chain",
|
|
30
|
+
2: "The credential in hand hashes to the bound hash(C) and states the bound limits, payees and mandate key",
|
|
31
31
|
3: "The credential's signatures: the issuer's under a trusted key that is the one recorded at binding, the owner's under the key the issuer bound",
|
|
32
|
-
4: "Each payment and refusal is the
|
|
32
|
+
4: "Each payment and refusal is the mandate key's per-payment mandate, rebuilt from its transaction, inside the mandate at its block",
|
|
33
33
|
};
|
|
34
|
-
const erc3009Abi = parseAbi([
|
|
35
|
-
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature)",
|
|
36
|
-
"function transferWithAuthorizationWithPolicy(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature, bytes policyData)",
|
|
37
|
-
"function receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature)",
|
|
38
|
-
"function receiveWithAuthorizationWithPolicy(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature, bytes policyData)",
|
|
39
|
-
]);
|
|
40
34
|
const short = (hex) => `${hex.slice(0, 10)}…`;
|
|
41
35
|
/** A reason as the facilitator names it to a payer. */
|
|
42
36
|
const named = (reason) => (reason === "None" ? "none" : REFUSALS[reason].reason);
|
|
@@ -77,45 +71,30 @@ function reasonContradiction(reason, pull, m, at) {
|
|
|
77
71
|
return `it breaks the mandate: ${named(first)}`;
|
|
78
72
|
return REASONS.indexOf(first) < REASONS.indexOf(reason) ? `${named(first)} holds, and Mandate Binding checks it before ${named(reason)}` : undefined;
|
|
79
73
|
}
|
|
80
|
-
/** The
|
|
74
|
+
/** The selector a payment's calldata carries: `pull` on the principal, whoever submitted it. */
|
|
81
75
|
const PULL_SELECTOR = toFunctionSelector(getAbiItem({ abi: mandateBindingAbi, name: "pull" }));
|
|
82
|
-
const ERC3009_SELECTORS = erc3009Abi.map((item) => toFunctionSelector(item));
|
|
83
76
|
/**
|
|
84
|
-
* Every pull
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
77
|
+
* Every pull that `input` carries: at the top level, or inside whatever
|
|
78
|
+
* wrapped the call (a multicall, a sponsor's batch, the quorum's own
|
|
79
|
+
* execute). Each occurrence of the selector in the calldata is tried as a
|
|
80
|
+
* call's start; the one the event's digest names is the payment's, and a
|
|
81
|
+
* forged candidate rebuilds to another digest.
|
|
89
82
|
*/
|
|
90
|
-
function carriedL3as(input
|
|
83
|
+
function carriedL3as(input) {
|
|
91
84
|
const found = [];
|
|
92
85
|
const body = input.slice(2);
|
|
93
|
-
for (
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
for (let at = body.indexOf(PULL_SELECTOR.slice(2)); at !== -1; at = body.indexOf(PULL_SELECTOR.slice(2), at + 2)) {
|
|
87
|
+
if (at % 2 !== 0)
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
const call = decodeFunctionData({ abi: mandateBindingAbi, data: `0x${body.slice(at)}` });
|
|
91
|
+
if (call.functionName !== "pull")
|
|
96
92
|
continue;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
continue;
|
|
103
|
-
const [pull, fields] = call.args;
|
|
104
|
-
found.push({ pull: { ...pull }, fields: { ...fields }, direct: true });
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
const [from, payee, value, validAfter, validBefore, nonce, signature] = decodeFunctionData({ abi: erc3009Abi, data }).args;
|
|
108
|
-
if (!isAddressEqual(from, m.principal))
|
|
109
|
-
continue;
|
|
110
|
-
const { pull, fields } = decodeMandateBlob(signature);
|
|
111
|
-
const same = isAddressEqual(pull.to, payee) && pull.value === value && pull.validAfter === validAfter && pull.validBefore === validBefore && pull.nonce.toLowerCase() === nonce.toLowerCase();
|
|
112
|
-
if (same)
|
|
113
|
-
found.push({ pull, fields, direct: false });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// Not a call that starts here.
|
|
118
|
-
}
|
|
93
|
+
const [pull, fields] = call.args;
|
|
94
|
+
found.push({ pull: { ...pull }, fields: { ...fields } });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Not a call that starts here.
|
|
119
98
|
}
|
|
120
99
|
}
|
|
121
100
|
return found;
|
|
@@ -124,11 +103,10 @@ function carriedL3as(input, m) {
|
|
|
124
103
|
* The candidate that is the outcome the event records: it rebuilds to the
|
|
125
104
|
* recorded digest, moves the mandate's token, and its nonce commits to the
|
|
126
105
|
* mandate and that digest (the L3a hashes neither the token nor the nonce,
|
|
127
|
-
* so a copy with either changed is not it); among those, one
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* payment. One another mandate's terms cannot rebuild is not a candidate.
|
|
106
|
+
* so a copy with either changed is not it); among those, one signed by the
|
|
107
|
+
* agent key comes first, so unrelated calldata with the same L3a (a copy
|
|
108
|
+
* under another signature) cannot stand in for the payment. One another
|
|
109
|
+
* mandate's terms cannot rebuild is not a candidate.
|
|
132
110
|
*/
|
|
133
111
|
function carriedL3a(input, chainId, m, o) {
|
|
134
112
|
const rank = (c) => {
|
|
@@ -138,13 +116,13 @@ function carriedL3a(input, chainId, m, o) {
|
|
|
138
116
|
return 0;
|
|
139
117
|
if (!isAddressEqual(c.pull.token, m.constraints.token) || mandateNonce(m.ref, l3a.digest) !== c.pull.nonce.toLowerCase())
|
|
140
118
|
return 0;
|
|
141
|
-
return 1 + (
|
|
119
|
+
return 1 + (verifyL3aSignature(m.agentKey, l3a.digest, c.fields.r, c.fields.s) ? 1 : 0);
|
|
142
120
|
}
|
|
143
121
|
catch {
|
|
144
122
|
return 0;
|
|
145
123
|
}
|
|
146
124
|
};
|
|
147
|
-
const candidates = carriedL3as(input
|
|
125
|
+
const candidates = carriedL3as(input);
|
|
148
126
|
let carried;
|
|
149
127
|
let best = 0;
|
|
150
128
|
for (const c of candidates) {
|
|
@@ -153,7 +131,7 @@ function carriedL3a(input, chainId, m, o) {
|
|
|
153
131
|
[carried, best] = [c, r];
|
|
154
132
|
}
|
|
155
133
|
if (carried === undefined)
|
|
156
|
-
throw new Error(candidates.length === 0 ? "the transaction carries no pull on the principal
|
|
134
|
+
throw new Error(candidates.length === 0 ? "the transaction carries no pull on the principal" : "no pull the transaction carries rebuilds to the per-payment mandate the event records");
|
|
157
135
|
return carried;
|
|
158
136
|
}
|
|
159
137
|
/**
|
|
@@ -208,7 +186,7 @@ async function boundMandate(client, principal, ref, factory, toBlock) {
|
|
|
208
186
|
const agentKey = decodeAgentKey(log.args.agentKey);
|
|
209
187
|
const problems = [];
|
|
210
188
|
if (mandateReference(principal, agentKey, log.args.hashC).toLowerCase() !== ref.toLowerCase())
|
|
211
|
-
problems.push("the reference does not derive from the principal, the
|
|
189
|
+
problems.push("the reference does not derive from the principal, the mandate key and hash(C)");
|
|
212
190
|
const revoked = events.find((e) => e.eventName === "MandateRevoked");
|
|
213
191
|
const replaced = events.find((e) => e.eventName === "MandateReplaced");
|
|
214
192
|
const revokedBlock = revoked?.blockNumber ?? undefined;
|
|
@@ -238,28 +216,26 @@ function outcomesOf(events, only) {
|
|
|
238
216
|
if (log.blockNumber === null || log.transactionHash === null)
|
|
239
217
|
continue;
|
|
240
218
|
if (log.eventName === "MandatePayment") {
|
|
241
|
-
const { to, amount, l3aDigest, agentKey,
|
|
242
|
-
outcomes.push({ ...position(log), kind: "payment", txHash: log.transactionHash, block: log.blockNumber, to, amount, l3aDigest, agentKey,
|
|
219
|
+
const { to, amount, l3aDigest, agentKey, spent, occurrences } = log.args;
|
|
220
|
+
outcomes.push({ ...position(log), kind: "payment", txHash: log.transactionHash, block: log.blockNumber, to, amount, l3aDigest, agentKey, spent, occurrences });
|
|
243
221
|
}
|
|
244
222
|
else if (log.eventName === "MandateRefused") {
|
|
245
223
|
const { to, amount, l3aDigest, agentKey, reason } = log.args;
|
|
246
|
-
outcomes.push({ ...position(log), kind: "refusal", txHash: log.transactionHash, block: log.blockNumber, to, amount, l3aDigest, agentKey,
|
|
224
|
+
outcomes.push({ ...position(log), kind: "refusal", txHash: log.transactionHash, block: log.blockNumber, to, amount, l3aDigest, agentKey, reason: reasonName(reason) });
|
|
247
225
|
}
|
|
248
226
|
}
|
|
249
227
|
outcomes.sort((a, b) => before(a, b) ? -1 : before(b, a) ? 1 : 0);
|
|
250
228
|
return only === undefined ? outcomes : outcomes.filter((o) => o.txHash.toLowerCase() === only.toLowerCase());
|
|
251
229
|
}
|
|
252
230
|
async function verifyOutcome(client, chainId, m, o, one) {
|
|
253
|
-
const verified = { kind: o.kind, txHash: o.txHash, block: o.block,
|
|
231
|
+
const verified = { kind: o.kind, txHash: o.txHash, block: o.block, to: o.to, amount: o.amount, ...(o.reason === undefined ? {} : { reason: o.reason }), problems: [] };
|
|
254
232
|
const found = verified.problems;
|
|
255
233
|
const agentKey = decodeAgentKey(o.agentKey);
|
|
256
234
|
if (!sameAgentKey(agentKey, m.agentKey))
|
|
257
|
-
found.push("not the bound
|
|
235
|
+
found.push("not the bound mandate key");
|
|
258
236
|
try {
|
|
259
237
|
const [tx, block] = await Promise.all([client.getTransaction({ hash: o.txHash }), client.getBlock({ blockNumber: o.block })]);
|
|
260
|
-
const { pull, fields
|
|
261
|
-
if (direct !== o.direct)
|
|
262
|
-
found.push(direct ? "recorded as relayed, carried as a pull" : "recorded as a pull, carried as a relayed authorisation");
|
|
238
|
+
const { pull, fields } = carriedL3a(tx.input, chainId, m, o);
|
|
263
239
|
if (!isAddressEqual(pull.token, m.constraints.token))
|
|
264
240
|
found.push("another token than the mandate's");
|
|
265
241
|
const receipt = await client.getTransactionReceipt({ hash: o.txHash });
|
|
@@ -278,7 +254,7 @@ async function verifyOutcome(client, chainId, m, o, one) {
|
|
|
278
254
|
if (mandateNonce(m.ref, l3a.digest) !== pull.nonce.toLowerCase())
|
|
279
255
|
found.push("its nonce does not commit to the mandate and the per-payment mandate");
|
|
280
256
|
if (!verifyL3aSignature(m.agentKey, l3a.digest, fields.r, fields.s))
|
|
281
|
-
found.push("not the bound
|
|
257
|
+
found.push("not the bound mandate key's signature");
|
|
282
258
|
if (!isAddressEqual(pull.to, o.to))
|
|
283
259
|
found.push("another payee than the event records");
|
|
284
260
|
if (pull.value !== o.amount)
|
|
@@ -341,7 +317,7 @@ async function verify(client, found, opts, only) {
|
|
|
341
317
|
skip(4, "no binding to check against");
|
|
342
318
|
return { ok: false, steps, payments: [] };
|
|
343
319
|
}
|
|
344
|
-
record(1, found.problems, `${short(m.ref)} bound in block ${m.block} by ${m.principal}: hash(C) ${m.hashC}, issuer key ${m.issuerKey},
|
|
320
|
+
record(1, found.problems, `${short(m.ref)} bound in block ${m.block} by ${m.principal}: hash(C) ${m.hashC}, issuer key ${m.issuerKey}, mandate key ${short(encodeAgentKey(m.agentKey))}, ${m.terms.payees.length} payee(s)` +
|
|
345
321
|
`${m.revokedBlock === undefined ? "" : `, revoked in block ${m.revokedBlock}`}${m.successor === undefined ? "" : `, replaced by ${short(m.successor)}`}`);
|
|
346
322
|
const c = opts.credential;
|
|
347
323
|
let credentialIsBound = false;
|
|
@@ -357,7 +333,7 @@ async function verify(client, found, opts, only) {
|
|
|
357
333
|
const stated = extractTerms(c);
|
|
358
334
|
const differs = [
|
|
359
335
|
["principal", isAddressEqual(stated.principal, m.principal)],
|
|
360
|
-
["
|
|
336
|
+
["mandate key", sameAgentKey(stated.agentKey, m.agentKey)],
|
|
361
337
|
["limits", jcs(toJson(stated.constraints)) === jcs(toJson(m.constraints))],
|
|
362
338
|
["payees", jcs(toJson(stated.terms)) === jcs(toJson(m.terms))],
|
|
363
339
|
].filter(([, same]) => !same);
|
|
@@ -369,7 +345,7 @@ async function verify(client, found, opts, only) {
|
|
|
369
345
|
}
|
|
370
346
|
}
|
|
371
347
|
credentialIsBound = problems.length === 0;
|
|
372
|
-
record(2, problems, "the credential hashes to the bound hash(C), and binds exactly the limits, payees and
|
|
348
|
+
record(2, problems, "the credential hashes to the bound hash(C), and binds exactly the limits, payees and mandate key it states");
|
|
373
349
|
}
|
|
374
350
|
if (c === undefined || !credentialIsBound)
|
|
375
351
|
skip(3, c === undefined ? "no credential in hand" : "the credential in hand is not the bound one");
|
|
@@ -401,7 +377,7 @@ async function verify(client, found, opts, only) {
|
|
|
401
377
|
if ((opts.requirements !== undefined || opts.checkoutMandate !== undefined) && outcomes.length > 1)
|
|
402
378
|
problems.push("requirements or a signed offer given, but more than one payment to check them against");
|
|
403
379
|
const paid = payments.filter((p) => p.kind === "payment").length;
|
|
404
|
-
record(4, problems, `${paid} payment(s) and ${payments.length - paid} refusal(s), each the bound
|
|
380
|
+
record(4, problems, `${paid} payment(s) and ${payments.length - paid} refusal(s), each the bound mandate key's per-payment mandate rebuilt byte for byte from its transaction, inside the mandate at its block`);
|
|
405
381
|
return { ok: steps.every((s) => s.outcome !== "fail"), steps, mandate: m, payments };
|
|
406
382
|
}
|
|
407
383
|
/** The factory the verifier trusts on `client`'s chain: the caller's, else the one the network table names. */
|
|
@@ -273,13 +273,13 @@ function resolveTerms({ l1, l2 }, c) {
|
|
|
273
273
|
const paymentJwk = obj(obj(paymentMandate.cnf, "payment cnf").jwk, "payment cnf.jwk");
|
|
274
274
|
const agentJwk = { ...p256Jwk(paymentJwk, "payment cnf.jwk", "MalformedMandate"), kid: str(paymentJwk, "kid") };
|
|
275
275
|
if (agentJwk.kid !== jwkThumbprint(agentJwk))
|
|
276
|
-
fail("MalformedMandate", "the
|
|
276
|
+
fail("MalformedMandate", "the mandate key's kid is not its RFC 7638 thumbprint");
|
|
277
277
|
if (jcs(checkoutJwk) !== jcs(paymentJwk))
|
|
278
|
-
fail("MalformedMandate", "the checkout and payment mandates name different
|
|
278
|
+
fail("MalformedMandate", "the checkout and payment mandates name different mandate keys");
|
|
279
279
|
if (l2.payload.aud !== agentJwk.kid)
|
|
280
|
-
fail("MalformedMandate", "layer2 aud is not the
|
|
280
|
+
fail("MalformedMandate", "layer2 aud is not the mandate key's kid");
|
|
281
281
|
if (/["\\]|[^\x20-\x7e]/.test(agentJwk.kid))
|
|
282
|
-
fail("MalformedMandate", "the
|
|
282
|
+
fail("MalformedMandate", "the mandate key's kid is not printable ASCII");
|
|
283
283
|
const instrumentObject = obj(paymentMandate.payment_instrument, "payment_instrument");
|
|
284
284
|
if (instrumentObject.type !== INSTRUMENT_TYPE)
|
|
285
285
|
fail("MalformedMandate", `payment_instrument.type must be ${INSTRUMENT_TYPE}`);
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClearingClientStore } from "./clearing-client.js";
|
|
2
|
+
import type { Hex } from "viem";
|
|
3
|
+
/** A persistent P-256 account and receipt journal for a single server process. */
|
|
4
|
+
export declare function openClearingWallet(directory: string): Promise<{
|
|
5
|
+
signer: {
|
|
6
|
+
publicKey: `0x${string}`;
|
|
7
|
+
sign(namespace: string, message: Hex): Hex;
|
|
8
|
+
};
|
|
9
|
+
store: ClearingClientStore;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}>;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { mkdir, open, readFile, rename } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { createClearingSigner } from "./clearing-codec.js";
|
|
6
|
+
/** A persistent P-256 account and receipt journal for a single server process. */
|
|
7
|
+
export async function openClearingWallet(directory) {
|
|
8
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
9
|
+
// SQLite holds an OS lock for the wallet lifetime and releases it on crash.
|
|
10
|
+
const owner = new DatabaseSync(join(directory, "owner.sqlite"));
|
|
11
|
+
try {
|
|
12
|
+
owner.exec("PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE; CREATE TABLE IF NOT EXISTS owner (id INTEGER)");
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
owner.close();
|
|
16
|
+
throw new Error("clearing wallet is already in use", { cause: error });
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const keyPath = join(directory, "account.key");
|
|
20
|
+
let key;
|
|
21
|
+
try {
|
|
22
|
+
key = (await readFile(keyPath, "utf8")).trim();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== "ENOENT")
|
|
26
|
+
throw error;
|
|
27
|
+
for (;;) {
|
|
28
|
+
key = `0x${randomBytes(32).toString("hex")}`;
|
|
29
|
+
try {
|
|
30
|
+
createClearingSigner(key);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* Rejection sample the scalar. */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const file = await open(keyPath, "wx", 0o600);
|
|
38
|
+
try {
|
|
39
|
+
await file.writeFile(key);
|
|
40
|
+
await file.sync();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await file.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const parent = await open(directory, "r");
|
|
47
|
+
try {
|
|
48
|
+
await parent.sync();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await parent.close();
|
|
52
|
+
}
|
|
53
|
+
const signer = createClearingSigner(key);
|
|
54
|
+
const statePath = join(directory, "payments.json");
|
|
55
|
+
const store = {
|
|
56
|
+
async load() {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(await readFile(statePath, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === "ENOENT")
|
|
62
|
+
return null;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
async save(state) {
|
|
67
|
+
const file = await open(statePath + ".next", "w", 0o600);
|
|
68
|
+
try {
|
|
69
|
+
await file.writeFile(JSON.stringify(state));
|
|
70
|
+
await file.sync();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await file.close();
|
|
74
|
+
}
|
|
75
|
+
await rename(statePath + ".next", statePath);
|
|
76
|
+
const parent = await open(directory, "r");
|
|
77
|
+
try {
|
|
78
|
+
await parent.sync();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await parent.close();
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
signer,
|
|
87
|
+
store,
|
|
88
|
+
async close() {
|
|
89
|
+
owner.close();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
owner.close();
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalntwx/metal-pay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Agent payments on Metal for the official x402 packages:
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Agent payments on Metal for the official x402 packages: native Bajillion clearing, on-chain exact payments, durable payer wallets, seller integration, and mandates, which let an agent pay from its owner's account within limits Metal enforces.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"directory": "sdks/metal-pay"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": "^22.13.0 || >=23.4.0"
|
|
14
|
+
},
|
|
12
15
|
"main": "./dist/index.js",
|
|
13
16
|
"types": "./dist/index.d.ts",
|
|
14
17
|
"exports": {
|
|
@@ -16,9 +19,9 @@
|
|
|
16
19
|
"types": "./dist/index.d.ts",
|
|
17
20
|
"default": "./dist/index.js"
|
|
18
21
|
},
|
|
19
|
-
"./
|
|
20
|
-
"types": "./dist/
|
|
21
|
-
"default": "./dist/
|
|
22
|
+
"./node": {
|
|
23
|
+
"types": "./dist/node.d.ts",
|
|
24
|
+
"default": "./dist/node.js"
|
|
22
25
|
},
|
|
23
26
|
"./mandates": {
|
|
24
27
|
"types": "./dist/mandates/index.d.ts",
|
|
@@ -26,21 +29,25 @@
|
|
|
26
29
|
}
|
|
27
30
|
},
|
|
28
31
|
"files": [
|
|
29
|
-
"dist"
|
|
32
|
+
"dist",
|
|
33
|
+
"src"
|
|
30
34
|
],
|
|
31
35
|
"dependencies": {
|
|
32
|
-
"@noble/curves": "
|
|
33
|
-
"@x402/core": "^2.21.0",
|
|
34
|
-
"@x402/evm": "^2.21.0",
|
|
35
|
-
"@x402/fetch": "^2.21.0"
|
|
36
|
+
"@noble/curves": "1.9.7"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
39
|
+
"@x402/core": ">=2.28.0 <3",
|
|
40
|
+
"@x402/evm": ">=2.28.0 <3",
|
|
41
|
+
"@x402/fetch": ">=2.28.0 <3",
|
|
38
42
|
"viem": "^2"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@metalntwx/metal-ts": "0.3.2",
|
|
42
46
|
"@types/node": "^24.13.2",
|
|
43
|
-
"@x402/
|
|
47
|
+
"@x402/core": "2.28.0",
|
|
48
|
+
"@x402/evm": "2.28.0",
|
|
49
|
+
"@x402/fetch": "2.28.0",
|
|
50
|
+
"@x402/hono": "2.28.0",
|
|
44
51
|
"hono": "4.13.7",
|
|
45
52
|
"tsx": "^4.23.0",
|
|
46
53
|
"typescript": "~6.0.2",
|