@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/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, getAddress, hexToBigInt, isAddressEqual, numberToHex, erc20Abi, parseAbi, parseEventLogs, slice, toEventSelector, 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
|
-
import { verifyCheckoutMandate, x402Offer, lineItemOf } from "./checkout.js";
|
|
16
|
+
import { checkoutHash, readL3b, 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,9 +131,53 @@ 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
|
}
|
|
137
|
+
const MANDATE_EVENTS = new Set(mandateBindingAbi.filter((item) => item.type === "event").map((item) => toEventSelector(item)));
|
|
138
|
+
/** The events a MetalAccount emits besides Mandate Binding's: its quorum, scheduling, sessions and OpenZeppelin bases. */
|
|
139
|
+
const ACCOUNT_EVENTS = new Set([
|
|
140
|
+
...MANDATE_EVENTS,
|
|
141
|
+
...parseAbi([
|
|
142
|
+
"event EIP712DomainChanged()",
|
|
143
|
+
"event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes returndata)",
|
|
144
|
+
"event ERC7913SignerAdded(bytes indexed signers)",
|
|
145
|
+
"event ERC7913SignerRemoved(bytes indexed signers)",
|
|
146
|
+
"event ERC7913SignerWeightChanged(bytes indexed signer, uint64 weight)",
|
|
147
|
+
"event ERC7913ThresholdSet(uint64 threshold)",
|
|
148
|
+
"event Executed(uint256 indexed nonce)",
|
|
149
|
+
"event Initialized(uint64 version)",
|
|
150
|
+
"event ScheduleCanceled(bytes32 indexed operationId)",
|
|
151
|
+
"event ScheduleConfigured(uint64 threshold, uint32 delay)",
|
|
152
|
+
"event Scheduled(bytes32 indexed operationId, uint256 indexed nonce, bytes32 mode, bytes executionData, uint48 readyAt)",
|
|
153
|
+
"event ScheduledExecuted(bytes32 indexed operationId)",
|
|
154
|
+
"event SessionGranted(address indexed agent, address indexed token, uint64 expiresAt, uint256 perTransferCap, uint256 periodBudget, uint32 period)",
|
|
155
|
+
"event SessionRevoked(address indexed agent, address indexed token)",
|
|
156
|
+
"event SessionTransfer(address indexed agent, address indexed token, address indexed to, uint256 amount)",
|
|
157
|
+
]).map((item) => toEventSelector(item)),
|
|
158
|
+
]);
|
|
159
|
+
/** What a verifier that cannot decode an account's events says: a deployed account of another version than the SDK's can emit them. */
|
|
160
|
+
const OTHER_VERSIONS = "the SDK and the deployed account may be different versions";
|
|
161
|
+
/** A log's place in the chain: its transaction and its index there. */
|
|
162
|
+
const placeOf = (log) => `${log.transactionHash}:${log.logIndex}`;
|
|
163
|
+
/**
|
|
164
|
+
* The account's events under `ref` that Mandate Binding's ABI does not
|
|
165
|
+
* decode, by topic. Each puts the reference in its first topic, so an
|
|
166
|
+
* account of another version emits its outcomes there under another topic,
|
|
167
|
+
* or under the same topic in another layout, and a scan that dropped them
|
|
168
|
+
* would see a mandate with no payments. Each log is matched to its decoding
|
|
169
|
+
* by its place.
|
|
170
|
+
*/
|
|
171
|
+
function undecoded(principal, ref, logs, decoded) {
|
|
172
|
+
const read = new Set(decoded.map(placeOf));
|
|
173
|
+
const counts = new Map();
|
|
174
|
+
for (const log of logs.map((raw) => formatLog(raw))) {
|
|
175
|
+
const topic = log.topics[0];
|
|
176
|
+
if (topic !== undefined && !read.has(placeOf(log)))
|
|
177
|
+
counts.set(topic, (counts.get(topic) ?? 0) + 1);
|
|
178
|
+
}
|
|
179
|
+
return [...counts].map(([topic, count]) => `the account ${getAddress(principal)} emitted ${count} event${count === 1 ? "" : "s"} under ${short(ref)} that this SDK does not decode, under topic ${topic}; ${OTHER_VERSIONS}`);
|
|
180
|
+
}
|
|
159
181
|
/**
|
|
160
182
|
* The mandate's events on the principal between two blocks, oldest first: one
|
|
161
183
|
* `eth_getLogs` for every event kind, since each puts the reference in its
|
|
@@ -165,11 +187,13 @@ function carriedL3a(input, chainId, m, o) {
|
|
|
165
187
|
*/
|
|
166
188
|
async function mandateEvents(client, principal, ref, fromBlock, toBlock) {
|
|
167
189
|
const out = [];
|
|
190
|
+
const raw = [];
|
|
168
191
|
let span = toBlock - fromBlock + 1n;
|
|
169
192
|
for (let start = fromBlock; start <= toBlock;) {
|
|
170
193
|
const end = start + span - 1n > toBlock ? toBlock : start + span - 1n;
|
|
171
194
|
try {
|
|
172
|
-
const logs = await client.request({ method: "eth_getLogs", params: [{ address: principal, fromBlock: numberToHex(start), toBlock: numberToHex(end), topics: [null, ref] }] });
|
|
195
|
+
const logs = (await client.request({ method: "eth_getLogs", params: [{ address: principal, fromBlock: numberToHex(start), toBlock: numberToHex(end), topics: [null, ref] }] }));
|
|
196
|
+
raw.push(...logs);
|
|
173
197
|
out.push(...parseEventLogs({ abi: mandateBindingAbi, logs: logs.map((log) => formatLog(log)), strict: true }));
|
|
174
198
|
start = end + 1n;
|
|
175
199
|
}
|
|
@@ -179,7 +203,7 @@ async function mandateEvents(client, principal, ref, fromBlock, toBlock) {
|
|
|
179
203
|
span /= 2n;
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
|
-
return out;
|
|
206
|
+
return { events: out, unknown: undecoded(principal, ref, raw, out) };
|
|
183
207
|
}
|
|
184
208
|
/**
|
|
185
209
|
* The mandate `ref` as the principal's account bound it: its state names the
|
|
@@ -189,26 +213,26 @@ async function mandateEvents(client, principal, ref, fromBlock, toBlock) {
|
|
|
189
213
|
async function boundMandate(client, principal, ref, factory, toBlock) {
|
|
190
214
|
const state = await client.readContract({ address: principal, abi: mandateBindingAbi, functionName: "mandates", args: [ref] });
|
|
191
215
|
if (!isAddressEqual(state.principal, principal))
|
|
192
|
-
return { events: [], problems: [`no mandate ${short(ref)} is bound on ${principal}`] };
|
|
216
|
+
return { events: [], unknown: [], problems: [`no mandate ${short(ref)} is bound on ${principal}`] };
|
|
193
217
|
const from = state.boundBlock;
|
|
194
218
|
const to = toBlock ?? (await client.getBlockNumber());
|
|
195
|
-
const events = await mandateEvents(client, principal, ref, from, to);
|
|
219
|
+
const { events, unknown } = await mandateEvents(client, principal, ref, from, to);
|
|
196
220
|
const bound = events.filter((e) => e.eventName === "MandateBound");
|
|
197
221
|
if (bound.length !== 1)
|
|
198
|
-
return { events, problems: [bound.length === 0 ? `no MandateBound for ${short(ref)} on ${principal} in block ${from}, where its state says it was bound` : `${bound.length} MandateBound events for ${short(ref)}`] };
|
|
222
|
+
return { events, unknown, problems: [bound.length === 0 ? `no MandateBound for ${short(ref)} on ${principal} in block ${from}, where its state says it was bound` : `${bound.length} MandateBound events for ${short(ref)}`] };
|
|
199
223
|
const [log] = bound;
|
|
200
224
|
if (log === undefined || log.eventName !== "MandateBound" || log.blockNumber === null || log.transactionHash === null)
|
|
201
|
-
return { events, problems: ["the MandateBound event is pending"] };
|
|
225
|
+
return { events, unknown, problems: ["the MandateBound event is pending"] };
|
|
202
226
|
try {
|
|
203
227
|
await assertMetalAccount(client, principal, factory);
|
|
204
228
|
}
|
|
205
229
|
catch (error) {
|
|
206
|
-
return { events, problems: [message(error)] };
|
|
230
|
+
return { events, unknown, problems: [message(error)] };
|
|
207
231
|
}
|
|
208
232
|
const agentKey = decodeAgentKey(log.args.agentKey);
|
|
209
233
|
const problems = [];
|
|
210
234
|
if (mandateReference(principal, agentKey, log.args.hashC).toLowerCase() !== ref.toLowerCase())
|
|
211
|
-
problems.push("the reference does not derive from the principal, the
|
|
235
|
+
problems.push("the reference does not derive from the principal, the mandate key and hash(C)");
|
|
212
236
|
const revoked = events.find((e) => e.eventName === "MandateRevoked");
|
|
213
237
|
const replaced = events.find((e) => e.eventName === "MandateReplaced");
|
|
214
238
|
const revokedBlock = revoked?.blockNumber ?? undefined;
|
|
@@ -228,6 +252,7 @@ async function boundMandate(client, principal, ref, factory, toBlock) {
|
|
|
228
252
|
...(successor === undefined ? {} : { successor }),
|
|
229
253
|
},
|
|
230
254
|
events,
|
|
255
|
+
unknown,
|
|
231
256
|
problems,
|
|
232
257
|
};
|
|
233
258
|
}
|
|
@@ -238,28 +263,26 @@ function outcomesOf(events, only) {
|
|
|
238
263
|
if (log.blockNumber === null || log.transactionHash === null)
|
|
239
264
|
continue;
|
|
240
265
|
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,
|
|
266
|
+
const { to, amount, l3aDigest, agentKey, spent, occurrences } = log.args;
|
|
267
|
+
outcomes.push({ ...position(log), kind: "payment", txHash: log.transactionHash, block: log.blockNumber, to, amount, l3aDigest, agentKey, spent, occurrences });
|
|
243
268
|
}
|
|
244
269
|
else if (log.eventName === "MandateRefused") {
|
|
245
270
|
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,
|
|
271
|
+
outcomes.push({ ...position(log), kind: "refusal", txHash: log.transactionHash, block: log.blockNumber, to, amount, l3aDigest, agentKey, reason: reasonName(reason) });
|
|
247
272
|
}
|
|
248
273
|
}
|
|
249
274
|
outcomes.sort((a, b) => before(a, b) ? -1 : before(b, a) ? 1 : 0);
|
|
250
275
|
return only === undefined ? outcomes : outcomes.filter((o) => o.txHash.toLowerCase() === only.toLowerCase());
|
|
251
276
|
}
|
|
252
277
|
async function verifyOutcome(client, chainId, m, o, one) {
|
|
253
|
-
const verified = { kind: o.kind, txHash: o.txHash, block: o.block,
|
|
278
|
+
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
279
|
const found = verified.problems;
|
|
255
280
|
const agentKey = decodeAgentKey(o.agentKey);
|
|
256
281
|
if (!sameAgentKey(agentKey, m.agentKey))
|
|
257
|
-
found.push("not the bound
|
|
282
|
+
found.push("not the bound mandate key");
|
|
258
283
|
try {
|
|
259
284
|
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");
|
|
285
|
+
const { pull, fields } = carriedL3a(tx.input, chainId, m, o);
|
|
263
286
|
if (!isAddressEqual(pull.token, m.constraints.token))
|
|
264
287
|
found.push("another token than the mandate's");
|
|
265
288
|
const receipt = await client.getTransactionReceipt({ hash: o.txHash });
|
|
@@ -278,12 +301,17 @@ async function verifyOutcome(client, chainId, m, o, one) {
|
|
|
278
301
|
if (mandateNonce(m.ref, l3a.digest) !== pull.nonce.toLowerCase())
|
|
279
302
|
found.push("its nonce does not commit to the mandate and the per-payment mandate");
|
|
280
303
|
if (!verifyL3aSignature(m.agentKey, l3a.digest, fields.r, fields.s))
|
|
281
|
-
found.push("not the bound
|
|
304
|
+
found.push("not the bound mandate key's signature");
|
|
282
305
|
if (!isAddressEqual(pull.to, o.to))
|
|
283
306
|
found.push("another payee than the event records");
|
|
284
307
|
if (pull.value !== o.amount)
|
|
285
308
|
found.push("another amount than the event records");
|
|
286
|
-
|
|
309
|
+
const other = one.checkoutMandate === undefined ? undefined : othersOrder(one.checkoutMandate, fields.transactionId, pull);
|
|
310
|
+
if (other !== undefined) {
|
|
311
|
+
// Its other checks would only list how that other payment differs.
|
|
312
|
+
found.push(`the checkout mandate belongs to another payment: ${other}`);
|
|
313
|
+
}
|
|
314
|
+
else if (one.checkoutMandate !== undefined) {
|
|
287
315
|
// The offer served, when the verifier served it; the signed order's own otherwise.
|
|
288
316
|
const offer = one.requirements !== null && typeof one.requirements === "object" && !Array.isArray(one.requirements) ? x402Offer(one.requirements) : undefined;
|
|
289
317
|
found.push(...verifyCheckoutMandate(one.checkoutMandate, {
|
|
@@ -329,19 +357,40 @@ async function verifyOutcome(client, chainId, m, o, one) {
|
|
|
329
357
|
}
|
|
330
358
|
return verified;
|
|
331
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* How a checkout mandate that parses is another payment's: a checkout hash
|
|
362
|
+
* other than the per-payment mandate's `transaction_id`, or another window,
|
|
363
|
+
* as both are signed together for one payment. Undefined when it is this
|
|
364
|
+
* payment's, or does not parse, which its own check names.
|
|
365
|
+
*/
|
|
366
|
+
function othersOrder(checkoutMandate, transactionId, pull) {
|
|
367
|
+
try {
|
|
368
|
+
const l3b = readL3b(checkoutMandate);
|
|
369
|
+
if (!Number.isSafeInteger(l3b.iat) || !Number.isSafeInteger(l3b.exp))
|
|
370
|
+
return undefined;
|
|
371
|
+
const differs = [
|
|
372
|
+
...(checkoutHash(l3b.checkoutJwt) === transactionId.toLowerCase() ? [] : ["its checkout hash is not the transaction_id this payment's per-payment mandate signed"]),
|
|
373
|
+
...(BigInt(l3b.iat) === pull.validAfter && BigInt(l3b.exp) === pull.validBefore ? [] : ["its window is not this payment's"]),
|
|
374
|
+
];
|
|
375
|
+
return differs.length === 0 ? undefined : differs.join(", and ");
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
332
381
|
async function verify(client, found, opts, only) {
|
|
333
382
|
const steps = [];
|
|
334
383
|
const record = (step, problems, passed) => steps.push({ step, title: STEP_TITLES[step], outcome: problems.length > 0 ? "fail" : "pass", detail: problems.length > 0 ? problems.join("; ") : passed });
|
|
335
384
|
const skip = (step, why) => steps.push({ step, title: STEP_TITLES[step], outcome: "skipped", detail: why });
|
|
336
385
|
const m = found.mandate;
|
|
337
386
|
if (m === undefined) {
|
|
338
|
-
record(1, found.problems, "");
|
|
387
|
+
record(1, [...found.problems, ...found.unknown], "");
|
|
339
388
|
skip(2, "no binding to check against");
|
|
340
389
|
skip(3, "no binding to check against");
|
|
341
390
|
skip(4, "no binding to check against");
|
|
342
391
|
return { ok: false, steps, payments: [] };
|
|
343
392
|
}
|
|
344
|
-
record(1, found.problems, `${short(m.ref)} bound in block ${m.block} by ${m.principal}: hash(C) ${m.hashC}, issuer key ${m.issuerKey},
|
|
393
|
+
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
394
|
`${m.revokedBlock === undefined ? "" : `, revoked in block ${m.revokedBlock}`}${m.successor === undefined ? "" : `, replaced by ${short(m.successor)}`}`);
|
|
346
395
|
const c = opts.credential;
|
|
347
396
|
let credentialIsBound = false;
|
|
@@ -357,7 +406,7 @@ async function verify(client, found, opts, only) {
|
|
|
357
406
|
const stated = extractTerms(c);
|
|
358
407
|
const differs = [
|
|
359
408
|
["principal", isAddressEqual(stated.principal, m.principal)],
|
|
360
|
-
["
|
|
409
|
+
["mandate key", sameAgentKey(stated.agentKey, m.agentKey)],
|
|
361
410
|
["limits", jcs(toJson(stated.constraints)) === jcs(toJson(m.constraints))],
|
|
362
411
|
["payees", jcs(toJson(stated.terms)) === jcs(toJson(m.terms))],
|
|
363
412
|
].filter(([, same]) => !same);
|
|
@@ -369,7 +418,7 @@ async function verify(client, found, opts, only) {
|
|
|
369
418
|
}
|
|
370
419
|
}
|
|
371
420
|
credentialIsBound = problems.length === 0;
|
|
372
|
-
record(2, problems, "the credential hashes to the bound hash(C), and binds exactly the limits, payees and
|
|
421
|
+
record(2, problems, "the credential hashes to the bound hash(C), and binds exactly the limits, payees and mandate key it states");
|
|
373
422
|
}
|
|
374
423
|
if (c === undefined || !credentialIsBound)
|
|
375
424
|
skip(3, c === undefined ? "no credential in hand" : "the credential in hand is not the bound one");
|
|
@@ -395,13 +444,15 @@ async function verify(client, found, opts, only) {
|
|
|
395
444
|
const one = outcomes.length === 1 ? { ...opts, ...(checkoutSdHash === undefined ? {} : { checkoutSdHash }) } : {};
|
|
396
445
|
for (const o of outcomes)
|
|
397
446
|
payments.push(await verifyOutcome(client, chainId, m, o, one));
|
|
398
|
-
const problems = payments.filter((p) => p.problems.length > 0).map((p) => `${p.kind} in ${short(p.txHash)}: ${p.problems.join(", ")}`);
|
|
447
|
+
const problems = [...found.unknown, ...payments.filter((p) => p.problems.length > 0).map((p) => `${p.kind} in ${short(p.txHash)}: ${p.problems.join(", ")}`)];
|
|
399
448
|
if (only !== undefined && outcomes.length === 0)
|
|
400
449
|
problems.push(`transaction ${only.tx} carries no payment or refusal under ${short(m.ref)}`);
|
|
401
450
|
if ((opts.requirements !== undefined || opts.checkoutMandate !== undefined) && outcomes.length > 1)
|
|
402
451
|
problems.push("requirements or a signed offer given, but more than one payment to check them against");
|
|
403
452
|
const paid = payments.filter((p) => p.kind === "payment").length;
|
|
404
|
-
|
|
453
|
+
// One payment's check says whether it covered the signed order.
|
|
454
|
+
const order = only === undefined ? "" : opts.checkoutMandate === undefined ? "; no checkout mandate was given, so the order was not checked" : "; the checkout mandate is this payment's signed order";
|
|
455
|
+
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${order}`);
|
|
405
456
|
return { ok: steps.every((s) => s.outcome !== "fail"), steps, mandate: m, payments };
|
|
406
457
|
}
|
|
407
458
|
/** The factory the verifier trusts on `client`'s chain: the caller's, else the one the network table names. */
|
|
@@ -442,12 +493,26 @@ export async function verifyMandatePayment(client, opts) {
|
|
|
442
493
|
// passes on part of a transaction's outcomes.
|
|
443
494
|
const clone = metalAccountCode(await client.readContract({ address: accountFactory, abi: metalAccountFactoryAbi, functionName: "implementation" }));
|
|
444
495
|
const emitters = new Map();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
496
|
+
const trusted = async (address) => {
|
|
497
|
+
if (!emitters.has(address))
|
|
498
|
+
emitters.set(address, (await client.getCode({ address: address }))?.toLowerCase() === clone);
|
|
499
|
+
return emitters.get(address) === true;
|
|
500
|
+
};
|
|
501
|
+
for (const address of new Set(logs.map((log) => log.address.toLowerCase())))
|
|
502
|
+
await trusted(address);
|
|
448
503
|
const carried = logs.filter((log) => emitters.get(log.address.toLowerCase()) === true);
|
|
449
|
-
if (carried.length === 0)
|
|
450
|
-
|
|
504
|
+
if (carried.length === 0) {
|
|
505
|
+
// An account of another version than the SDK's emits its outcomes under
|
|
506
|
+
// topics the ABI does not decode, or under its topics in another layout.
|
|
507
|
+
const read = new Set(parseEventLogs({ abi: mandateBindingAbi, logs: receipt.logs, strict: true }).map(placeOf));
|
|
508
|
+
for (const log of receipt.logs) {
|
|
509
|
+
const topic = log.topics[0]?.toLowerCase();
|
|
510
|
+
if (topic === undefined || (MANDATE_EVENTS.has(topic) ? read.has(placeOf(log)) : ACCOUNT_EVENTS.has(topic)) || !(await trusted(log.address.toLowerCase())))
|
|
511
|
+
continue;
|
|
512
|
+
throw new Error(`transaction ${opts.tx} carries no payment or refusal this SDK decodes: the account ${getAddress(log.address)} emitted an event under topic ${topic}, which this SDK does not decode; ${OTHER_VERSIONS}`);
|
|
513
|
+
}
|
|
514
|
+
throw new Error(logs.length === 0 ? `transaction ${opts.tx} carries no payment or refusal under a mandate` : `transaction ${opts.tx} carries mandate outcomes only from ${[...new Set(logs.map((log) => log.address.toLowerCase()))].join(", ")}, no account of the trusted factory ${accountFactory}`);
|
|
515
|
+
}
|
|
451
516
|
// One transaction may settle payments under several mandates (a sponsor's
|
|
452
517
|
// batch). `ref` or the credential in hand names the one to verify; a
|
|
453
518
|
// credential that names none of them is still checked against a single
|
|
@@ -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/networks.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Network } from "@x402/core/types";
|
|
2
|
-
import type
|
|
2
|
+
import { type Address, type HttpTransport } from "viem";
|
|
3
3
|
export type MetalNetworkName = "devnet" | "testnet";
|
|
4
4
|
export interface MetalNetwork {
|
|
5
5
|
name: MetalNetworkName;
|
|
@@ -55,6 +55,14 @@ export declare function resolveNetwork(entry: MetalNetworkName | MetalNetwork):
|
|
|
55
55
|
* METAL_RPC_URL"), how the caller passes it.
|
|
56
56
|
*/
|
|
57
57
|
export declare function rpcUrlFor(network: MetalNetwork, override: string | undefined, how: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* viem's `http` transport to `rpcUrl`, whose errors do not carry the URL: it
|
|
60
|
+
* can hold the endpoint's credential, so each error it throws, and each of
|
|
61
|
+
* that error's causes, names it as `[RPC URL hidden]`. An endpoint that
|
|
62
|
+
* gives no answer, or a server error, is named as one that could not be
|
|
63
|
+
* reached.
|
|
64
|
+
*/
|
|
65
|
+
export declare function rpcTransport(rpcUrl: string): HttpTransport;
|
|
58
66
|
/** A dollar token payers authorize ERC-3009 transfers of. */
|
|
59
67
|
export interface DollarToken {
|
|
60
68
|
address: `0x${string}`;
|
package/dist/networks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HttpRequestError, TimeoutError, http } from "viem";
|
|
1
2
|
/**
|
|
2
3
|
* The MetalAccountFactory to trust on `network`: `override` when given, else
|
|
3
4
|
* the network's own. On a network with neither it throws, saying how the
|
|
@@ -32,7 +33,7 @@ export const METAL_NETWORKS = {
|
|
|
32
33
|
facilitatorUrl: "https://x402-testnet.metalntwx.com/x402/facilitator",
|
|
33
34
|
// What `just estate-deploy-account-factory testnet` recorded in
|
|
34
35
|
// deploy/estate/environments/testnet/account-factory.json for this chain.
|
|
35
|
-
accountFactory: "
|
|
36
|
+
accountFactory: "0xcEc6e13AC50078AE3f1e163d5fbd257DAcED8023",
|
|
36
37
|
},
|
|
37
38
|
};
|
|
38
39
|
/** Resolve a name or a CAIP-2 identifier to a Metal network; undefined for any other chain. */
|
|
@@ -61,6 +62,105 @@ export function rpcUrlFor(network, override, how) {
|
|
|
61
62
|
return network.rpc.url;
|
|
62
63
|
throw new Error(`${network.name} needs an RPC endpoint of your own: sign up at ${network.rpc.consoleUrl}, create an endpoint, then ${how}`);
|
|
63
64
|
}
|
|
65
|
+
/** What an RPC URL reads as in an error. */
|
|
66
|
+
const HIDDEN_RPC_URL = "[RPC URL hidden]";
|
|
67
|
+
/**
|
|
68
|
+
* viem's `http` transport to `rpcUrl`, whose errors do not carry the URL: it
|
|
69
|
+
* can hold the endpoint's credential, so each error it throws, and each of
|
|
70
|
+
* that error's causes, names it as `[RPC URL hidden]`. An endpoint that
|
|
71
|
+
* gives no answer, or a server error, is named as one that could not be
|
|
72
|
+
* reached.
|
|
73
|
+
*/
|
|
74
|
+
export function rpcTransport(rpcUrl) {
|
|
75
|
+
const transport = http(rpcUrl);
|
|
76
|
+
const parsed = URL.canParse(rpcUrl) ? new URL(rpcUrl) : undefined;
|
|
77
|
+
const host = parsed?.host;
|
|
78
|
+
// What an endpoint can echo of the URL without its host: the path and
|
|
79
|
+
// query, which can hold a key, and the user and password.
|
|
80
|
+
const decoded = (text) => {
|
|
81
|
+
try {
|
|
82
|
+
return decodeURIComponent(text);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const path = parsed ? parsed.pathname + parsed.search : "";
|
|
89
|
+
const secrets = [rpcUrl];
|
|
90
|
+
if (path.length > 1)
|
|
91
|
+
secrets.push(path);
|
|
92
|
+
// A key is often echoed alone: each path segment and query value long enough to be one.
|
|
93
|
+
if (parsed) {
|
|
94
|
+
const parts = [...decoded(parsed.pathname).split("/"), ...[...parsed.searchParams.values()]];
|
|
95
|
+
for (const part of parts)
|
|
96
|
+
if (part.length >= 8)
|
|
97
|
+
secrets.push(part);
|
|
98
|
+
}
|
|
99
|
+
if (parsed?.username || parsed?.password) {
|
|
100
|
+
const [user, password] = [decoded(parsed.username), decoded(parsed.password)];
|
|
101
|
+
secrets.push(`${user}:${password}`);
|
|
102
|
+
if (password.length >= 4)
|
|
103
|
+
secrets.push(password);
|
|
104
|
+
}
|
|
105
|
+
// viem prints the URL as given, or without its user and password.
|
|
106
|
+
const hide = (text) => secrets
|
|
107
|
+
.reduce((hidden, secret) => hidden.replaceAll(secret, HIDDEN_RPC_URL), text)
|
|
108
|
+
.replace(/https?:\/\/[^\s"'<>]+/g, (url) => (URL.canParse(url) && new URL(url).host === host ? HIDDEN_RPC_URL : url));
|
|
109
|
+
// Every string the error holds, in its fields, its causes, its arrays and
|
|
110
|
+
// its response headers, as viem or the endpoint put it there: the fields
|
|
111
|
+
// that name the URL are not a list to keep.
|
|
112
|
+
const scrub = (value, seen) => {
|
|
113
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
114
|
+
return;
|
|
115
|
+
seen.add(value);
|
|
116
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
117
|
+
const fields = value;
|
|
118
|
+
let field;
|
|
119
|
+
try {
|
|
120
|
+
field = fields[key];
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let hidden = field;
|
|
126
|
+
if (typeof field === "string")
|
|
127
|
+
hidden = hide(field);
|
|
128
|
+
else if (field instanceof Headers) {
|
|
129
|
+
const entries = [...field].map(([name, text]) => [name, hide(text)]);
|
|
130
|
+
if (entries.some(([name, text]) => field.get(name) !== text))
|
|
131
|
+
hidden = new Headers(entries);
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
scrub(field, seen);
|
|
135
|
+
if (hidden !== field)
|
|
136
|
+
try {
|
|
137
|
+
fields[key] = hidden;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// A property that cannot be written holds what it held.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
return ((options) => {
|
|
145
|
+
const made = transport(options);
|
|
146
|
+
return {
|
|
147
|
+
...made,
|
|
148
|
+
request: (async (args, requestOptions) => {
|
|
149
|
+
try {
|
|
150
|
+
return await made.request(args, requestOptions);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof TimeoutError || (error instanceof HttpRequestError && (error.status === undefined || error.status >= 500))) {
|
|
154
|
+
error.shortMessage = `the RPC endpoint could not be reached: ${error.shortMessage}`;
|
|
155
|
+
error.message = `the RPC endpoint could not be reached: ${error.message}`;
|
|
156
|
+
}
|
|
157
|
+
scrub(error, new Set());
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
64
164
|
/** The default gas token and the asset every offer here is priced in. */
|
|
65
165
|
export const USDZ = {
|
|
66
166
|
address: "0x000000000000000000000000000000000000B5d5",
|
|
@@ -95,8 +195,9 @@ export function usdzBaseUnits(amount) {
|
|
|
95
195
|
/** Format base units back to a decimal `*USDZ` string for humans, trailing zeros trimmed. */
|
|
96
196
|
export function formatUsdz(amount) {
|
|
97
197
|
const units = BigInt(amount);
|
|
198
|
+
const magnitude = units < 0n ? -units : units;
|
|
98
199
|
const scale = 10n ** BigInt(USDZ.decimals);
|
|
99
|
-
const whole =
|
|
100
|
-
const fraction = (
|
|
101
|
-
return fraction ? `${whole}.${fraction}` : whole
|
|
200
|
+
const whole = magnitude / scale;
|
|
201
|
+
const fraction = (magnitude % scale).toString().padStart(USDZ.decimals, "0").replace(/0+$/, "");
|
|
202
|
+
return `${units < 0n ? "-" : ""}${fraction ? `${whole}.${fraction}` : whole}`;
|
|
102
203
|
}
|
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
|
+
}>;
|