@metalntwx/metal-pay 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -38
- package/dist/clearing-abi.d.ts +1836 -0
- package/dist/clearing-abi.js +2361 -0
- package/dist/clearing-client.d.ts +96 -159
- package/dist/clearing-client.js +412 -553
- package/dist/clearing-codec.d.ts +57 -0
- package/dist/clearing-codec.js +173 -0
- package/dist/clearing-custody.d.ts +202 -0
- package/dist/clearing-custody.js +552 -0
- package/dist/clearing-server.d.ts +30 -49
- package/dist/clearing-server.js +87 -67
- package/dist/clearing-x402.d.ts +38 -0
- package/dist/clearing-x402.js +249 -0
- package/dist/client.d.ts +14 -15
- package/dist/client.js +53 -22
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mandates/account.d.ts +9 -4
- package/dist/mandates/account.js +15 -11
- 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 +4 -4
- package/dist/mandates/verify.d.ts +4 -6
- package/dist/mandates/verify.js +147 -82
- package/dist/mandates/vi/credential.js +4 -4
- package/dist/networks.d.ts +9 -1
- package/dist/networks.js +105 -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.test.ts +850 -0
- package/src/clearing-client.ts +612 -0
- package/src/clearing-codec.test.ts +57 -0
- package/src/clearing-codec.ts +251 -0
- package/src/clearing-custody.test.ts +763 -0
- package/src/clearing-custody.ts +743 -0
- package/src/clearing-fixture.json +37 -0
- package/src/clearing-server.test.ts +450 -0
- package/src/clearing-server.ts +123 -0
- package/src/clearing-x402.test.ts +181 -0
- package/src/clearing-x402.ts +328 -0
- package/src/client.test.ts +333 -0
- package/src/client.ts +195 -0
- package/src/index.ts +33 -0
- package/src/mandates/account.ts +95 -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 +625 -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 +177 -0
- package/src/networks.ts +242 -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/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { METAL_NETWORKS, USDZ, formatUsdz, metalNetwork, usdzBaseUnits, accountFactoryFor, rpcUrlFor, } from "./networks.js";
|
|
2
2
|
export { DEFAULT_MAX_PAYMENT, createMetalPaymentClient, registerMetalNetworks, } from "./client.js";
|
|
3
3
|
export { metalUsd, registerMetalScheme, sellerToken, } from "./server.js";
|
|
4
|
-
export * from "./clearing.js";
|
|
4
|
+
export * from "./clearing-codec.js";
|
|
5
|
+
export { clearingAbi } from "./clearing-abi.js";
|
|
5
6
|
export * from "./clearing-client.js";
|
|
7
|
+
export * from "./clearing-x402.js";
|
|
6
8
|
export * from "./clearing-server.js";
|
|
9
|
+
export * from "./clearing-custody.js";
|
|
@@ -70,6 +70,9 @@ export declare const metalAccountAbi: readonly [{
|
|
|
70
70
|
}, {
|
|
71
71
|
readonly type: "uint256";
|
|
72
72
|
readonly name: "nonce";
|
|
73
|
+
}, {
|
|
74
|
+
readonly type: "uint64";
|
|
75
|
+
readonly name: "validUntil";
|
|
73
76
|
}, {
|
|
74
77
|
readonly type: "bytes";
|
|
75
78
|
readonly name: "signature";
|
|
@@ -84,7 +87,9 @@ export declare function assertMetalAccount(client: PublicClient, account: Addres
|
|
|
84
87
|
/** The gas an executeSigned adds to the call it carries, by the quorum signature's signers. */
|
|
85
88
|
export declare function quorumGas(signature: Hex): bigint;
|
|
86
89
|
/** Self-calls approved by the account's existing ERC-7913 quorum, in one batch
|
|
87
|
-
* that runs whole or not at all. The
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
90
|
+
* that runs whole or not at all. The account refuses the signed batch once the
|
|
91
|
+
* chain's block timestamp passes `validUntil` (unix seconds): pick a deadline
|
|
92
|
+
* just long enough to collect the signatures and submit. The signing callback
|
|
93
|
+
* may collect mixed keys, weights and passkeys; it returns the normal
|
|
94
|
+
* abi.encode(bytes[] signers, bytes[] signatures) account signature. */
|
|
95
|
+
export declare function authorizeMandateCall(client: PublicClient, factory: Address, call: MandateCall | readonly MandateCall[], validUntil: bigint, signQuorum: (digest: Hex) => Promise<Hex>): Promise<MandateCall>;
|
package/dist/mandates/account.js
CHANGED
|
@@ -6,7 +6,7 @@ export const metalAccountFactoryAbi = parseAbi([
|
|
|
6
6
|
]);
|
|
7
7
|
export const metalAccountAbi = parseAbi([
|
|
8
8
|
"function nonces(address owner, uint192 key) view returns (uint256)",
|
|
9
|
-
"function executeSigned(bytes32 mode, bytes executionData, uint256 nonce, bytes signature)",
|
|
9
|
+
"function executeSigned(bytes32 mode, bytes executionData, uint256 nonce, uint64 validUntil, bytes signature)",
|
|
10
10
|
]);
|
|
11
11
|
export const ACCOUNT_BATCH_MODE = "0x0100000000000000000000000000000000000000000000000000000000000000";
|
|
12
12
|
/** Exact immutable ERC-1167 clone emitted by MetalAccountFactory. */
|
|
@@ -56,10 +56,12 @@ export function quorumGas(signature) {
|
|
|
56
56
|
return gas;
|
|
57
57
|
}
|
|
58
58
|
/** Self-calls approved by the account's existing ERC-7913 quorum, in one batch
|
|
59
|
-
* that runs whole or not at all. The
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
59
|
+
* that runs whole or not at all. The account refuses the signed batch once the
|
|
60
|
+
* chain's block timestamp passes `validUntil` (unix seconds): pick a deadline
|
|
61
|
+
* just long enough to collect the signatures and submit. The signing callback
|
|
62
|
+
* may collect mixed keys, weights and passkeys; it returns the normal
|
|
63
|
+
* abi.encode(bytes[] signers, bytes[] signatures) account signature. */
|
|
64
|
+
export async function authorizeMandateCall(client, factory, call, validUntil, signQuorum) {
|
|
63
65
|
const chainId = client.chain?.id;
|
|
64
66
|
if (chainId === undefined)
|
|
65
67
|
throw new Error("the RPC client names no chain");
|
|
@@ -71,9 +73,11 @@ export async function authorizeMandateCall(client, factory, call, signQuorum) {
|
|
|
71
73
|
if (calls.some((c) => !isAddressEqual(c.to, account)))
|
|
72
74
|
throw new Error("every call in one batch goes to the same account");
|
|
73
75
|
await assertMetalAccount(client, account, factory);
|
|
74
|
-
// A fresh
|
|
75
|
-
//
|
|
76
|
-
|
|
76
|
+
// A fresh nonce lane per call: two administration calls in flight, or one
|
|
77
|
+
// beside the owner's own operations on lane 0, never claim one nonce. A
|
|
78
|
+
// quorum's lane keeps the key's top bit clear; set, it is a lane for
|
|
79
|
+
// signatures below the threshold, which the account refuses for the quorum.
|
|
80
|
+
const lane = hexToBigInt(bytesToHex(crypto.getRandomValues(new Uint8Array(24)))) >> 1n;
|
|
77
81
|
const sequence = await client.readContract({ address: account, abi: metalAccountAbi, functionName: "nonces", args: [account, lane] });
|
|
78
82
|
const nonce = (lane << 64n) | sequence;
|
|
79
83
|
const executionData = encodeAbiParameters(parseAbiParameters("(address target, uint256 value, bytes callData)[]"), [calls.map((c) => ({ target: account, value: 0n, callData: c.data }))]);
|
|
@@ -81,11 +85,11 @@ export async function authorizeMandateCall(client, factory, call, signQuorum) {
|
|
|
81
85
|
// it could have the quorum sign a batch of its own choosing.
|
|
82
86
|
const digest = hashTypedData({
|
|
83
87
|
domain: { name: "MetalAccount", version: "1", chainId, verifyingContract: account },
|
|
84
|
-
types: { Execute: [{ name: "mode", type: "bytes32" }, { name: "executionData", type: "bytes" }, { name: "nonce", type: "uint256" }] },
|
|
88
|
+
types: { Execute: [{ name: "mode", type: "bytes32" }, { name: "executionData", type: "bytes" }, { name: "nonce", type: "uint256" }, { name: "validUntil", type: "uint64" }] },
|
|
85
89
|
primaryType: "Execute",
|
|
86
|
-
message: { mode: ACCOUNT_BATCH_MODE, executionData, nonce },
|
|
90
|
+
message: { mode: ACCOUNT_BATCH_MODE, executionData, nonce, validUntil },
|
|
87
91
|
});
|
|
88
92
|
const signature = await signQuorum(digest);
|
|
89
93
|
const gas = calls.reduce((sum, c) => sum + c.gas, 0n) + quorumGas(signature);
|
|
90
|
-
return { ...first, gas, data: encodeFunctionData({ abi: metalAccountAbi, functionName: "executeSigned", args: [ACCOUNT_BATCH_MODE, executionData, nonce, signature] }) };
|
|
94
|
+
return { ...first, gas, data: encodeFunctionData({ abi: metalAccountAbi, functionName: "executeSigned", args: [ACCOUNT_BATCH_MODE, executionData, nonce, validUntil, signature] }) };
|
|
91
95
|
}
|
|
@@ -2,14 +2,6 @@ import { type Address, type Hex } from "viem";
|
|
|
2
2
|
/** Domain tag for the payment-only ERC-1271 path on MetalAccount. */
|
|
3
3
|
export declare const MANDATE_SIGNATURE_TAG: Hex;
|
|
4
4
|
export declare const mandateBindingAbi: readonly [{
|
|
5
|
-
readonly name: "mip20TransferHandler";
|
|
6
|
-
readonly type: "function";
|
|
7
|
-
readonly stateMutability: "pure";
|
|
8
|
-
readonly inputs: readonly [];
|
|
9
|
-
readonly outputs: readonly [{
|
|
10
|
-
readonly type: "bytes4";
|
|
11
|
-
}];
|
|
12
|
-
}, {
|
|
13
5
|
readonly name: "mandateReference";
|
|
14
6
|
readonly type: "function";
|
|
15
7
|
readonly stateMutability: "pure";
|
|
@@ -402,7 +394,10 @@ export declare const mandateBindingAbi: readonly [{
|
|
|
402
394
|
}];
|
|
403
395
|
readonly name: "l";
|
|
404
396
|
}];
|
|
405
|
-
readonly outputs: readonly [
|
|
397
|
+
readonly outputs: readonly [{
|
|
398
|
+
readonly type: "bool";
|
|
399
|
+
readonly name: "paid";
|
|
400
|
+
}];
|
|
406
401
|
}, {
|
|
407
402
|
readonly name: "MandateBound";
|
|
408
403
|
readonly type: "event";
|
|
@@ -503,9 +498,6 @@ export declare const mandateBindingAbi: readonly [{
|
|
|
503
498
|
}, {
|
|
504
499
|
readonly type: "uint256";
|
|
505
500
|
readonly name: "occurrences";
|
|
506
|
-
}, {
|
|
507
|
-
readonly type: "bool";
|
|
508
|
-
readonly name: "direct";
|
|
509
501
|
}];
|
|
510
502
|
}, {
|
|
511
503
|
readonly name: "MandateRefused";
|
|
@@ -619,21 +611,6 @@ export declare const mandateBindingAbi: readonly [{
|
|
|
619
611
|
readonly name: "RefusalLimitReached";
|
|
620
612
|
readonly type: "error";
|
|
621
613
|
readonly inputs: readonly [];
|
|
622
|
-
}, {
|
|
623
|
-
readonly name: "NotApproved";
|
|
624
|
-
readonly type: "error";
|
|
625
|
-
readonly inputs: readonly [];
|
|
626
|
-
}, {
|
|
627
|
-
readonly name: "MandateBreach";
|
|
628
|
-
readonly type: "error";
|
|
629
|
-
readonly inputs: readonly [{
|
|
630
|
-
readonly type: "uint8";
|
|
631
|
-
readonly name: "reason";
|
|
632
|
-
}];
|
|
633
|
-
}, {
|
|
634
|
-
readonly name: "UnauthorizedHookCaller";
|
|
635
|
-
readonly type: "error";
|
|
636
|
-
readonly inputs: readonly [];
|
|
637
614
|
}];
|
|
638
615
|
/** `MandateTypes.Reason` in declaration order, which is the order Mandate Binding checks them in; `None` is a pass. */
|
|
639
616
|
export declare const REASONS: readonly ["None", "NoMandate", "Revoked", "Expired", "BadSignature", "NonceUsed", "OutsideWindow", "PayeeNotAllowed", "OverPerPaymentCap", "OverBudget", "OutsideRecurrence", "OverOccurrences"];
|
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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The seller's side of a signed order: a check, before the facilitator is
|
|
2
2
|
// asked to verify, that a payment under a mandate carries the agent's signed
|
|
3
3
|
// offer for this very offer, under the agent key the payer's account binds.
|
|
4
|
-
import { createPublicClient, getAddress,
|
|
5
|
-
import { accountFactoryFor, metalNetwork } from "../networks.js";
|
|
4
|
+
import { createPublicClient, getAddress, isAddress, isHex, isAddressEqual } from "viem";
|
|
5
|
+
import { accountFactoryFor, metalNetwork, rpcTransport } from "../networks.js";
|
|
6
6
|
import { assertMetalAccount } from "./account.js";
|
|
7
7
|
import { referencePrefix } from "./binding.js";
|
|
8
8
|
import { decodeMandateBlob } from "./l3a.js";
|
|
@@ -13,14 +13,14 @@ 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
|
|
20
20
|
* route that does not ask is left to the facilitator.
|
|
21
21
|
*/
|
|
22
22
|
export function requireSignedOffer(server, config) {
|
|
23
|
-
const publicClient = "publicClient" in config ? config.publicClient : createPublicClient({ transport:
|
|
23
|
+
const publicClient = "publicClient" in config ? config.publicClient : createPublicClient({ transport: rpcTransport(config.rpcUrl) });
|
|
24
24
|
const refuse = (reason) => ({ abort: true, reason });
|
|
25
25
|
// x402 treats a hook that throws as one that passed, so a check that cannot
|
|
26
26
|
// finish refuses the payment instead. The refusal reaches the payer, so it
|
|
@@ -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. */
|