@me.jango/signa 0.0.0-stage → 0.4.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 +81 -2
- package/core/controller.d.ts +43 -0
- package/core/index.d.ts +79 -0
- package/core/index.js +347 -0
- package/index.d.ts +1607 -0
- package/index.js +3074 -0
- package/package.json +20 -4
- package/react/index.d.ts +34 -0
- package/react/index.js +195 -0
- package/wagmi/index.d.ts +26 -0
- package/wagmi/index.js +97 -0
package/index.js
ADDED
|
@@ -0,0 +1,3074 @@
|
|
|
1
|
+
// src/rest/client/wallet.ts
|
|
2
|
+
import { toHex as toHex7 } from "viem";
|
|
3
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
4
|
+
|
|
5
|
+
// src/rest/client/index.ts
|
|
6
|
+
import { getAddress as getAddress5, isAddress as isAddress9, keccak256 as keccak2569 } from "viem";
|
|
7
|
+
|
|
8
|
+
// src/rest/auth/signatures.ts
|
|
9
|
+
import {
|
|
10
|
+
getAddress,
|
|
11
|
+
hashTypedData,
|
|
12
|
+
isAddress,
|
|
13
|
+
keccak256,
|
|
14
|
+
recoverTypedDataAddress,
|
|
15
|
+
stringToHex,
|
|
16
|
+
toHex
|
|
17
|
+
} from "viem";
|
|
18
|
+
|
|
19
|
+
// src/rest/auth/shared.ts
|
|
20
|
+
var ALL_BOT_SCOPES = ["read", "plan", "relay"];
|
|
21
|
+
var RestAuthError = class extends Error {
|
|
22
|
+
constructor(code, status, message2) {
|
|
23
|
+
super(message2);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.name = "RestAuthError";
|
|
27
|
+
}
|
|
28
|
+
code;
|
|
29
|
+
status;
|
|
30
|
+
};
|
|
31
|
+
function isCanonicalGrantScopes(scopes) {
|
|
32
|
+
return Array.isArray(scopes) && scopes.length >= 1 && scopes.length <= ALL_BOT_SCOPES.length && ALL_BOT_SCOPES.slice(0, scopes.length).every((scope, index) => scopes[index] === scope);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/rest/auth/signatures.ts
|
|
36
|
+
var REST_AUTH_HEADERS = {
|
|
37
|
+
account: "X-Juicebox-Account",
|
|
38
|
+
signer: "X-Juicebox-Signer",
|
|
39
|
+
grant: "X-Juicebox-Grant",
|
|
40
|
+
issuedAt: "X-Juicebox-Issued-At",
|
|
41
|
+
expiresAt: "X-Juicebox-Expires-At",
|
|
42
|
+
nonce: "X-Juicebox-Nonce",
|
|
43
|
+
signature: "X-Juicebox-Signature",
|
|
44
|
+
idempotencyKey: "Idempotency-Key"
|
|
45
|
+
};
|
|
46
|
+
var requestTypes = {
|
|
47
|
+
CenterRequest: [
|
|
48
|
+
{ name: "audience", type: "string" },
|
|
49
|
+
{ name: "accountId", type: "string" },
|
|
50
|
+
{ name: "signer", type: "address" },
|
|
51
|
+
{ name: "grantId", type: "string" },
|
|
52
|
+
{ name: "method", type: "string" },
|
|
53
|
+
{ name: "requestTarget", type: "string" },
|
|
54
|
+
{ name: "contentType", type: "string" },
|
|
55
|
+
{ name: "bodyHash", type: "bytes32" },
|
|
56
|
+
{ name: "issuedAt", type: "uint64" },
|
|
57
|
+
{ name: "expiresAt", type: "uint64" },
|
|
58
|
+
{ name: "nonce", type: "bytes32" },
|
|
59
|
+
{ name: "idempotencyKey", type: "string" }
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
var botTypes = {
|
|
63
|
+
CenterBotProof: [
|
|
64
|
+
{ name: "audience", type: "string" },
|
|
65
|
+
{ name: "accountId", type: "string" },
|
|
66
|
+
{ name: "botAddress", type: "address" },
|
|
67
|
+
{ name: "scopes", type: "string[]" },
|
|
68
|
+
{ name: "expiresAt", type: "uint64" },
|
|
69
|
+
{ name: "label", type: "string" },
|
|
70
|
+
{ name: "ownerRequestNonce", type: "bytes32" }
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
function invalid(message2 = "Invalid signed request") {
|
|
74
|
+
throw new RestAuthError("INVALID_AUTH", 400, message2);
|
|
75
|
+
}
|
|
76
|
+
function validateAudience(audience) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(audience);
|
|
80
|
+
} catch {
|
|
81
|
+
return invalid("Invalid authentication audience");
|
|
82
|
+
}
|
|
83
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash || parsed.protocol !== "https:" && !(parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname))) {
|
|
84
|
+
return invalid("Authentication audience must be HTTPS or local HTTP without credentials, query, or fragment");
|
|
85
|
+
}
|
|
86
|
+
return audience.replace(/\/$/, "");
|
|
87
|
+
}
|
|
88
|
+
function accountIdFor(owner, authorityChainId) {
|
|
89
|
+
if (!Number.isSafeInteger(authorityChainId) || authorityChainId <= 0 || typeof owner !== "string" || !isAddress(owner)) {
|
|
90
|
+
return invalid("Invalid account owner or authority chain");
|
|
91
|
+
}
|
|
92
|
+
return `eip155:${authorityChainId}:${owner.toLowerCase()}`;
|
|
93
|
+
}
|
|
94
|
+
function parseAccountId(accountId) {
|
|
95
|
+
const match = /^eip155:([1-9][0-9]{0,15}):(0x[0-9a-f]{40})$/.exec(accountId);
|
|
96
|
+
if (!match) return invalid("Invalid account identity");
|
|
97
|
+
const authorityChainId = Number(match[1]);
|
|
98
|
+
if (!Number.isSafeInteger(authorityChainId)) return invalid("Invalid authority chain");
|
|
99
|
+
return { ownerAddress: getAddress(match[2]), authorityChainId };
|
|
100
|
+
}
|
|
101
|
+
function domain(audience, accountId) {
|
|
102
|
+
return {
|
|
103
|
+
name: "Juicebox Center REST",
|
|
104
|
+
version: "1",
|
|
105
|
+
chainId: parseAccountId(accountId).authorityChainId,
|
|
106
|
+
salt: keccak256(stringToHex(audience))
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function buildRequestTypedData(audience, claims2) {
|
|
110
|
+
audience = validateAudience(audience);
|
|
111
|
+
return {
|
|
112
|
+
domain: domain(audience, claims2.accountId),
|
|
113
|
+
types: requestTypes,
|
|
114
|
+
primaryType: "CenterRequest",
|
|
115
|
+
message: {
|
|
116
|
+
...claims2,
|
|
117
|
+
audience,
|
|
118
|
+
issuedAt: BigInt(claims2.issuedAt),
|
|
119
|
+
expiresAt: BigInt(claims2.expiresAt)
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function buildBotProofTypedData(audience, proof) {
|
|
124
|
+
audience = validateAudience(audience);
|
|
125
|
+
return {
|
|
126
|
+
domain: domain(audience, proof.accountId),
|
|
127
|
+
types: botTypes,
|
|
128
|
+
primaryType: "CenterBotProof",
|
|
129
|
+
message: { ...proof, audience, expiresAt: BigInt(proof.expiresAt) }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function newRequestNonce() {
|
|
133
|
+
return toHex(globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/rest/approvals.ts
|
|
137
|
+
import {
|
|
138
|
+
hashTypedData as hashTypedData2,
|
|
139
|
+
keccak256 as keccak2562,
|
|
140
|
+
recoverAddress,
|
|
141
|
+
sha256,
|
|
142
|
+
stringToHex as stringToHex2
|
|
143
|
+
} from "viem";
|
|
144
|
+
|
|
145
|
+
// src/rest/core.ts
|
|
146
|
+
var RestError = class extends Error {
|
|
147
|
+
constructor(status, code, message2, details) {
|
|
148
|
+
super(message2);
|
|
149
|
+
this.status = status;
|
|
150
|
+
this.code = code;
|
|
151
|
+
this.details = details;
|
|
152
|
+
this.name = "RestError";
|
|
153
|
+
}
|
|
154
|
+
status;
|
|
155
|
+
code;
|
|
156
|
+
details;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/rest/approvals.ts
|
|
160
|
+
var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
161
|
+
var bytes32 = /^0x[0-9a-fA-F]{64}$/;
|
|
162
|
+
var types = {
|
|
163
|
+
CenterTransactionApproval: [
|
|
164
|
+
{ name: "audience", type: "string" },
|
|
165
|
+
{ name: "accountId", type: "string" },
|
|
166
|
+
{ name: "principalId", type: "string" },
|
|
167
|
+
{ name: "planId", type: "string" },
|
|
168
|
+
{ name: "commitment", type: "bytes32" },
|
|
169
|
+
{ name: "stepIndex", type: "uint32" },
|
|
170
|
+
{ name: "transactionHash", type: "bytes32" },
|
|
171
|
+
{ name: "issuedAt", type: "uint64" },
|
|
172
|
+
{ name: "expiresAt", type: "uint64" },
|
|
173
|
+
{ name: "nonce", type: "bytes32" }
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
var sponsorshipTypes = {
|
|
177
|
+
CenterSponsorshipApproval: [
|
|
178
|
+
{ name: "audience", type: "string" },
|
|
179
|
+
{ name: "accountId", type: "string" },
|
|
180
|
+
{ name: "principalId", type: "string" },
|
|
181
|
+
{ name: "sponsorshipId", type: "string" },
|
|
182
|
+
{ name: "commitment", type: "bytes32" },
|
|
183
|
+
{ name: "submissionHash", type: "bytes32" },
|
|
184
|
+
{ name: "issuedAt", type: "uint64" },
|
|
185
|
+
{ name: "expiresAt", type: "uint64" },
|
|
186
|
+
{ name: "nonce", type: "bytes32" }
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
function invalid2(message2 = "The owner approval is malformed") {
|
|
190
|
+
throw new RestError(400, "INVALID_OWNER_APPROVAL", message2);
|
|
191
|
+
}
|
|
192
|
+
function time(value) {
|
|
193
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
194
|
+
}
|
|
195
|
+
function identity(input) {
|
|
196
|
+
if (!input || typeof input !== "object" || typeof input.accountId !== "string") return invalid2();
|
|
197
|
+
try {
|
|
198
|
+
parseAccountId(input.accountId);
|
|
199
|
+
} catch {
|
|
200
|
+
return invalid2("The approval account identity is invalid");
|
|
201
|
+
}
|
|
202
|
+
if (typeof input.principalId !== "string" || !(input.principalId === `owner:${input.accountId}` || input.principalId.startsWith("bot:") && uuid.test(input.principalId.slice(4)))) {
|
|
203
|
+
return invalid2("The approval must identify an account principal");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function binding(input) {
|
|
207
|
+
identity(input);
|
|
208
|
+
if (typeof input.planId !== "string" || !uuid.test(input.planId) || typeof input.commitment !== "string" || !bytes32.test(input.commitment) || typeof input.transactionHash !== "string" || !bytes32.test(input.transactionHash) || !Number.isSafeInteger(input.stepIndex) || Object.is(input.stepIndex, -0) || input.stepIndex < 0 || input.stepIndex > 31) {
|
|
209
|
+
return invalid2("The approval must identify an account principal, plan, and exact transaction step");
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
accountId: input.accountId,
|
|
213
|
+
principalId: input.principalId,
|
|
214
|
+
planId: input.planId,
|
|
215
|
+
commitment: input.commitment,
|
|
216
|
+
stepIndex: input.stepIndex,
|
|
217
|
+
transactionHash: input.transactionHash
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function claims(input) {
|
|
221
|
+
const bound = binding(input);
|
|
222
|
+
return { ...bound, ...window(input) };
|
|
223
|
+
}
|
|
224
|
+
function window(input) {
|
|
225
|
+
if (!time(input.issuedAt) || !time(input.expiresAt) || input.expiresAt <= input.issuedAt || input.expiresAt - input.issuedAt > 300 || typeof input.nonce !== "string" || !/^0x[0-9a-f]{64}$/.test(input.nonce)) {
|
|
226
|
+
return invalid2("The owner approval requires a validity window of at most five minutes and a 32-byte nonce");
|
|
227
|
+
}
|
|
228
|
+
return { issuedAt: input.issuedAt, expiresAt: input.expiresAt, nonce: input.nonce };
|
|
229
|
+
}
|
|
230
|
+
function buildTransactionApprovalTypedData(audience, input) {
|
|
231
|
+
audience = validateAudience(audience);
|
|
232
|
+
const value = claims(input);
|
|
233
|
+
return {
|
|
234
|
+
domain: {
|
|
235
|
+
name: "Juicebox Center REST",
|
|
236
|
+
version: "1",
|
|
237
|
+
chainId: parseAccountId(value.accountId).authorityChainId,
|
|
238
|
+
salt: keccak2562(stringToHex2(audience))
|
|
239
|
+
},
|
|
240
|
+
types,
|
|
241
|
+
primaryType: "CenterTransactionApproval",
|
|
242
|
+
message: { ...value, audience, issuedAt: BigInt(value.issuedAt), expiresAt: BigInt(value.expiresAt) }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function sponsorshipBinding(input) {
|
|
246
|
+
identity(input);
|
|
247
|
+
if (typeof input.sponsorshipId !== "string" || !uuid.test(input.sponsorshipId) || typeof input.commitment !== "string" || !bytes32.test(input.commitment) || typeof input.submissionHash !== "string" || !bytes32.test(input.submissionHash)) return invalid2();
|
|
248
|
+
return {
|
|
249
|
+
accountId: input.accountId,
|
|
250
|
+
principalId: input.principalId,
|
|
251
|
+
sponsorshipId: input.sponsorshipId,
|
|
252
|
+
commitment: input.commitment,
|
|
253
|
+
submissionHash: input.submissionHash
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function buildSponsorshipApprovalTypedData(audience, input) {
|
|
257
|
+
audience = validateAudience(audience);
|
|
258
|
+
const value = { ...sponsorshipBinding(input), ...window(input) };
|
|
259
|
+
return {
|
|
260
|
+
domain: {
|
|
261
|
+
name: "Juicebox Center REST",
|
|
262
|
+
version: "1",
|
|
263
|
+
chainId: parseAccountId(value.accountId).authorityChainId,
|
|
264
|
+
salt: keccak2562(stringToHex2(audience))
|
|
265
|
+
},
|
|
266
|
+
types: sponsorshipTypes,
|
|
267
|
+
primaryType: "CenterSponsorshipApproval",
|
|
268
|
+
message: { ...value, audience, issuedAt: BigInt(value.issuedAt), expiresAt: BigInt(value.expiresAt) }
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function sponsorshipSubmissionHash(commitment, signatures) {
|
|
272
|
+
if (typeof commitment !== "string" || !bytes32.test(commitment) || !Array.isArray(signatures) || signatures.length < 1 || signatures.length > 4 || Array.from(signatures).some((signature2) => typeof signature2 !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature2))) return invalid2();
|
|
273
|
+
return sha256(stringToHex2(JSON.stringify({ commitment: commitment.toLowerCase(), signatures: signatures.map((signature2) => signature2.toLowerCase()) })));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/rest/client/smartAccounts.ts
|
|
277
|
+
import {
|
|
278
|
+
concatHex as concatHex4,
|
|
279
|
+
hashTypedData as hashTypedData4,
|
|
280
|
+
isAddress as isAddress6,
|
|
281
|
+
keccak256 as keccak2565,
|
|
282
|
+
recoverAddress as recoverAddress3,
|
|
283
|
+
stringToHex as stringToHex4
|
|
284
|
+
} from "viem";
|
|
285
|
+
|
|
286
|
+
// src/rest/smartAccounts/accountExecution.ts
|
|
287
|
+
import {
|
|
288
|
+
concatHex,
|
|
289
|
+
decodeAbiParameters,
|
|
290
|
+
decodeFunctionData,
|
|
291
|
+
encodeAbiParameters as encodeAbiParameters2,
|
|
292
|
+
encodeFunctionData,
|
|
293
|
+
getAddress as getAddress3,
|
|
294
|
+
hashMessage,
|
|
295
|
+
hashTypedData as hashTypedData3,
|
|
296
|
+
recoverAddress as recoverAddress2,
|
|
297
|
+
size,
|
|
298
|
+
sliceHex,
|
|
299
|
+
toHex as toHex3
|
|
300
|
+
} from "viem";
|
|
301
|
+
|
|
302
|
+
// src/rest/protocol/abi.ts
|
|
303
|
+
import {
|
|
304
|
+
getAddress as getAddress2,
|
|
305
|
+
isAddress as isAddress2
|
|
306
|
+
} from "viem";
|
|
307
|
+
var invalid3 = (path, message2) => {
|
|
308
|
+
throw new RestError(400, "ABI_ARGUMENT_INVALID", `${path}: ${message2}`);
|
|
309
|
+
};
|
|
310
|
+
function integer(value, path, signed = false, bits = 256) {
|
|
311
|
+
if (typeof value !== "string" || value.length > 79 || !(signed ? /^(0|-[1-9][0-9]*|[1-9][0-9]*)$/ : /^(0|[1-9][0-9]*)$/).test(
|
|
312
|
+
value
|
|
313
|
+
))
|
|
314
|
+
return invalid3(
|
|
315
|
+
path,
|
|
316
|
+
"Use an exact canonical decimal integer string; JSON numbers, exponents and leading zeros are not accepted."
|
|
317
|
+
);
|
|
318
|
+
const n = BigInt(value);
|
|
319
|
+
const minimum = signed ? -(1n << BigInt(bits - 1)) : 0n;
|
|
320
|
+
const maximum = signed ? (1n << BigInt(bits - 1)) - 1n : (1n << BigInt(bits)) - 1n;
|
|
321
|
+
if (n < minimum || n > maximum)
|
|
322
|
+
return invalid3(
|
|
323
|
+
path,
|
|
324
|
+
`Value does not fit ${signed ? "int" : "uint"}${bits}.`
|
|
325
|
+
);
|
|
326
|
+
return n;
|
|
327
|
+
}
|
|
328
|
+
function address(value, path, nonzero = false) {
|
|
329
|
+
if (typeof value !== "string" || !isAddress2(value, { strict: true }))
|
|
330
|
+
return invalid3(
|
|
331
|
+
path,
|
|
332
|
+
"Use a valid EVM address, with a valid checksum when mixed case."
|
|
333
|
+
);
|
|
334
|
+
const result = getAddress2(value);
|
|
335
|
+
if (nonzero && BigInt(result) === 0n)
|
|
336
|
+
return invalid3(path, "Address must be nonzero.");
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
function exactObject(value, keys, path) {
|
|
340
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
341
|
+
return invalid3(path, "Expected a JSON object.");
|
|
342
|
+
if (Object.keys(value).some((key) => !keys.includes(key)))
|
|
343
|
+
return invalid3(
|
|
344
|
+
path,
|
|
345
|
+
"Unknown input field; caller ABIs, raw calldata and upstream URLs are not accepted."
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/rest/userOperations/codec.ts
|
|
350
|
+
import {
|
|
351
|
+
concat,
|
|
352
|
+
encodeAbiParameters,
|
|
353
|
+
isAddress as isAddress3,
|
|
354
|
+
keccak256 as keccak2563,
|
|
355
|
+
padHex,
|
|
356
|
+
sha256 as sha2562,
|
|
357
|
+
stringToHex as stringToHex3,
|
|
358
|
+
toHex as toHex2
|
|
359
|
+
} from "viem";
|
|
360
|
+
var USER_OPERATION_LIMITS = Object.freeze({
|
|
361
|
+
responseBytes: 1048576,
|
|
362
|
+
operationBytes: 262144,
|
|
363
|
+
calldataBytes: 65536,
|
|
364
|
+
signatureBytes: 16384,
|
|
365
|
+
paymasterBytes: 8192,
|
|
366
|
+
factoryBytes: 32768,
|
|
367
|
+
rpcCalls: 96
|
|
368
|
+
});
|
|
369
|
+
function uoError(code, message2, status = 422) {
|
|
370
|
+
throw new RestError(status, code, message2);
|
|
371
|
+
}
|
|
372
|
+
function uoObject(value) {
|
|
373
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
374
|
+
}
|
|
375
|
+
function uoQuantity(value, label, bits = 256) {
|
|
376
|
+
if (typeof value !== "string" || !/^0x(0|[1-9a-f][0-9a-f]{0,63})$/.test(value) || BigInt(value) >= 1n << BigInt(bits))
|
|
377
|
+
uoError("INVALID_USER_OPERATION", `Invalid canonical ${label}.`, 400);
|
|
378
|
+
return BigInt(value);
|
|
379
|
+
}
|
|
380
|
+
function uoBytes(value, label, maximum = USER_OPERATION_LIMITS.calldataBytes) {
|
|
381
|
+
if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(value) || (value.length - 2) / 2 > maximum)
|
|
382
|
+
uoError("INVALID_USER_OPERATION", `Invalid bounded ${label}.`, 400);
|
|
383
|
+
return value.toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
function uoAddress(value, label) {
|
|
386
|
+
if (typeof value !== "string" || !isAddress3(value) || /^0x0{40}$/i.test(value))
|
|
387
|
+
uoError("INVALID_USER_OPERATION", `Invalid ${label}.`, 400);
|
|
388
|
+
return value.toLowerCase();
|
|
389
|
+
}
|
|
390
|
+
function uoCanonical(value) {
|
|
391
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isSafeInteger(value))
|
|
392
|
+
return JSON.stringify(value);
|
|
393
|
+
if (Array.isArray(value)) return `[${value.map(uoCanonical).join(",")}]`;
|
|
394
|
+
if (uoObject(value))
|
|
395
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${uoCanonical(value[key])}`).join(",")}}`;
|
|
396
|
+
return uoError(
|
|
397
|
+
"INVALID_USER_OPERATION",
|
|
398
|
+
"Only bounded lossless JSON is accepted.",
|
|
399
|
+
400
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
var required = [
|
|
403
|
+
"sender",
|
|
404
|
+
"nonce",
|
|
405
|
+
"callData",
|
|
406
|
+
"callGasLimit",
|
|
407
|
+
"verificationGasLimit",
|
|
408
|
+
"preVerificationGas",
|
|
409
|
+
"maxFeePerGas",
|
|
410
|
+
"maxPriorityFeePerGas",
|
|
411
|
+
"signature"
|
|
412
|
+
];
|
|
413
|
+
var optional = [
|
|
414
|
+
"factory",
|
|
415
|
+
"factoryData",
|
|
416
|
+
"paymaster",
|
|
417
|
+
"paymasterVerificationGasLimit",
|
|
418
|
+
"paymasterPostOpGasLimit",
|
|
419
|
+
"paymasterData"
|
|
420
|
+
];
|
|
421
|
+
function normalizeUserOperation(value) {
|
|
422
|
+
if (!uoObject(value) || required.some((key) => !(key in value)) || Object.keys(value).some((key) => ![...required, ...optional].includes(key)))
|
|
423
|
+
uoError(
|
|
424
|
+
"INVALID_USER_OPERATION",
|
|
425
|
+
"Provide only the complete unpacked EntryPoint v0.7 fields.",
|
|
426
|
+
400
|
|
427
|
+
);
|
|
428
|
+
const result = {
|
|
429
|
+
sender: uoAddress(value.sender, "sender"),
|
|
430
|
+
nonce: toHex2(uoQuantity(value.nonce, "nonce")),
|
|
431
|
+
callData: uoBytes(value.callData, "account calldata"),
|
|
432
|
+
callGasLimit: toHex2(uoQuantity(value.callGasLimit, "call gas", 128)),
|
|
433
|
+
verificationGasLimit: toHex2(
|
|
434
|
+
uoQuantity(value.verificationGasLimit, "verification gas", 128)
|
|
435
|
+
),
|
|
436
|
+
preVerificationGas: toHex2(
|
|
437
|
+
uoQuantity(value.preVerificationGas, "pre-verification gas")
|
|
438
|
+
),
|
|
439
|
+
maxFeePerGas: toHex2(uoQuantity(value.maxFeePerGas, "maximum fee", 128)),
|
|
440
|
+
maxPriorityFeePerGas: toHex2(
|
|
441
|
+
uoQuantity(value.maxPriorityFeePerGas, "priority fee", 128)
|
|
442
|
+
),
|
|
443
|
+
signature: uoBytes(
|
|
444
|
+
value.signature,
|
|
445
|
+
"account signature",
|
|
446
|
+
USER_OPERATION_LIMITS.signatureBytes
|
|
447
|
+
)
|
|
448
|
+
};
|
|
449
|
+
if (BigInt(result.maxPriorityFeePerGas) > BigInt(result.maxFeePerGas))
|
|
450
|
+
uoError("INVALID_USER_OPERATION", "Priority fee exceeds maximum fee.", 400);
|
|
451
|
+
if ("factory" in value || "factoryData" in value) {
|
|
452
|
+
result.factory = uoAddress(value.factory, "factory");
|
|
453
|
+
if (result.factory === "0x0000000000000000000000000000000000007702" || result.factory === "0x7702000000000000000000000000000000000000")
|
|
454
|
+
uoError(
|
|
455
|
+
"USER_OPERATION_VERSION_UNSUPPORTED",
|
|
456
|
+
"EIP-7702 authorization is not an EntryPoint v0.7 factory.",
|
|
457
|
+
400
|
|
458
|
+
);
|
|
459
|
+
result.factoryData = uoBytes(
|
|
460
|
+
value.factoryData,
|
|
461
|
+
"factory calldata",
|
|
462
|
+
USER_OPERATION_LIMITS.factoryBytes
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (optional.slice(2).some((key) => key in value)) {
|
|
466
|
+
result.paymaster = uoAddress(value.paymaster, "paymaster");
|
|
467
|
+
result.paymasterData = uoBytes(
|
|
468
|
+
value.paymasterData,
|
|
469
|
+
"paymaster data",
|
|
470
|
+
USER_OPERATION_LIMITS.paymasterBytes
|
|
471
|
+
);
|
|
472
|
+
result.paymasterVerificationGasLimit = toHex2(
|
|
473
|
+
uoQuantity(
|
|
474
|
+
value.paymasterVerificationGasLimit,
|
|
475
|
+
"paymaster verification gas",
|
|
476
|
+
128
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
result.paymasterPostOpGasLimit = toHex2(
|
|
480
|
+
uoQuantity(value.paymasterPostOpGasLimit, "paymaster post-op gas", 128)
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
if (new TextEncoder().encode(uoCanonical(result)).byteLength > USER_OPERATION_LIMITS.operationBytes)
|
|
484
|
+
uoError(
|
|
485
|
+
"USER_OPERATION_TOO_LARGE",
|
|
486
|
+
"The user operation exceeds its byte limit.",
|
|
487
|
+
413
|
|
488
|
+
);
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
function packUserOperation(value) {
|
|
492
|
+
const op = normalizeUserOperation(value);
|
|
493
|
+
return {
|
|
494
|
+
sender: op.sender,
|
|
495
|
+
nonce: BigInt(op.nonce),
|
|
496
|
+
initCode: op.factory ? concat([op.factory, op.factoryData]) : "0x",
|
|
497
|
+
callData: op.callData,
|
|
498
|
+
accountGasLimits: concat([
|
|
499
|
+
padHex(op.verificationGasLimit, { size: 16 }),
|
|
500
|
+
padHex(op.callGasLimit, { size: 16 })
|
|
501
|
+
]),
|
|
502
|
+
preVerificationGas: BigInt(op.preVerificationGas),
|
|
503
|
+
gasFees: concat([
|
|
504
|
+
padHex(op.maxPriorityFeePerGas, { size: 16 }),
|
|
505
|
+
padHex(op.maxFeePerGas, { size: 16 })
|
|
506
|
+
]),
|
|
507
|
+
paymasterAndData: op.paymaster ? concat([
|
|
508
|
+
op.paymaster,
|
|
509
|
+
padHex(op.paymasterVerificationGasLimit, { size: 16 }),
|
|
510
|
+
padHex(op.paymasterPostOpGasLimit, { size: 16 }),
|
|
511
|
+
op.paymasterData
|
|
512
|
+
]) : "0x",
|
|
513
|
+
signature: op.signature
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function getUserOperationHash(operation, entryPoint, chainId) {
|
|
517
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0)
|
|
518
|
+
uoError(
|
|
519
|
+
"INVALID_USER_OPERATION_CHAIN",
|
|
520
|
+
"Expected a configured chain.",
|
|
521
|
+
400
|
|
522
|
+
);
|
|
523
|
+
const op = packUserOperation(operation);
|
|
524
|
+
const encoded = encodeAbiParameters(
|
|
525
|
+
[
|
|
526
|
+
{ type: "address" },
|
|
527
|
+
{ type: "uint256" },
|
|
528
|
+
{ type: "bytes32" },
|
|
529
|
+
{ type: "bytes32" },
|
|
530
|
+
{ type: "bytes32" },
|
|
531
|
+
{ type: "uint256" },
|
|
532
|
+
{ type: "bytes32" },
|
|
533
|
+
{ type: "bytes32" }
|
|
534
|
+
],
|
|
535
|
+
[
|
|
536
|
+
op.sender,
|
|
537
|
+
op.nonce,
|
|
538
|
+
keccak2563(op.initCode),
|
|
539
|
+
keccak2563(op.callData),
|
|
540
|
+
op.accountGasLimits,
|
|
541
|
+
op.preVerificationGas,
|
|
542
|
+
op.gasFees,
|
|
543
|
+
keccak2563(op.paymasterAndData)
|
|
544
|
+
]
|
|
545
|
+
);
|
|
546
|
+
return keccak2563(
|
|
547
|
+
encodeAbiParameters(
|
|
548
|
+
[{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
|
|
549
|
+
[
|
|
550
|
+
keccak2563(encoded),
|
|
551
|
+
uoAddress(entryPoint, "EntryPoint"),
|
|
552
|
+
BigInt(chainId)
|
|
553
|
+
]
|
|
554
|
+
)
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
function userOperationCommitment(operation, entryPoint, chainId) {
|
|
558
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0)
|
|
559
|
+
uoError(
|
|
560
|
+
"INVALID_USER_OPERATION_CHAIN",
|
|
561
|
+
"Expected a configured chain.",
|
|
562
|
+
400
|
|
563
|
+
);
|
|
564
|
+
return sha2562(
|
|
565
|
+
stringToHex3(
|
|
566
|
+
uoCanonical({
|
|
567
|
+
chainId,
|
|
568
|
+
entryPoint: uoAddress(entryPoint, "EntryPoint"),
|
|
569
|
+
operation: normalizeUserOperation(operation)
|
|
570
|
+
})
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
function userOperationMaximumCost(operation) {
|
|
575
|
+
const op = normalizeUserOperation(operation);
|
|
576
|
+
return (BigInt(op.callGasLimit) + BigInt(op.verificationGasLimit) + BigInt(op.preVerificationGas) + BigInt(op.paymasterVerificationGasLimit ?? "0x0") + BigInt(op.paymasterPostOpGasLimit ?? "0x0")) * BigInt(op.maxFeePerGas);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/rest/smartAccounts/accountExecution.ts
|
|
580
|
+
var SAFE7579_EXECUTION_ABI = [{ "name": "execute", "type": "function", "stateMutability": "nonpayable", "inputs": [{ "type": "bytes32", "name": "mode" }, { "type": "bytes", "name": "executionCalldata" }], "outputs": [] }];
|
|
581
|
+
var executionsParameter = [{ "type": "tuple[]", "components": [{ "type": "address", "name": "target" }, { "type": "uint256", "name": "value" }, { "type": "bytes", "name": "callData" }] }];
|
|
582
|
+
var SAFE7579_SINGLE_MODE = `0x${"00".repeat(32)}`;
|
|
583
|
+
var SAFE7579_BATCH_MODE = `0x01${"00".repeat(31)}`;
|
|
584
|
+
var MAX_EXECUTIONS = 16;
|
|
585
|
+
var MAX_EXECUTION_BYTES = 65536;
|
|
586
|
+
function fail(message2) {
|
|
587
|
+
throw new RestError(400, "SMART_ACCOUNT_ENVELOPE_INVALID", message2);
|
|
588
|
+
}
|
|
589
|
+
function hex(value, label, minimum, maximum) {
|
|
590
|
+
if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(value) || (value.length - 2) / 2 < minimum || (value.length - 2) / 2 > maximum)
|
|
591
|
+
fail(`${label} must contain ${minimum} to ${maximum} complete bytes.`);
|
|
592
|
+
return value.toLowerCase();
|
|
593
|
+
}
|
|
594
|
+
function callsChecked(calls) {
|
|
595
|
+
if (!Array.isArray(calls) || calls.length < 1 || calls.length > MAX_EXECUTIONS)
|
|
596
|
+
fail(`An execution must contain 1 to ${MAX_EXECUTIONS} exact calls.`);
|
|
597
|
+
return calls.map((call, index) => {
|
|
598
|
+
exactObject(call, ["target", "value", "callData"], `calls[${index}]`);
|
|
599
|
+
return {
|
|
600
|
+
target: address(call.target, `calls[${index}].target`, true),
|
|
601
|
+
value: integer(call.value, `calls[${index}].value`).toString(),
|
|
602
|
+
callData: hex(
|
|
603
|
+
call.callData,
|
|
604
|
+
`calls[${index}].callData`,
|
|
605
|
+
0,
|
|
606
|
+
MAX_EXECUTION_BYTES
|
|
607
|
+
)
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
function decodeSafe7579Nonce(nonce) {
|
|
612
|
+
const value = integer(nonce, "nonce");
|
|
613
|
+
const validator = getAddress3(toHex3(value >> 96n, { size: 20 }));
|
|
614
|
+
if (BigInt(validator) === 0n)
|
|
615
|
+
fail("A session nonce must select a nonzero validator.");
|
|
616
|
+
return {
|
|
617
|
+
validator,
|
|
618
|
+
lane: (value >> 64n & 0xffffffffn).toString(),
|
|
619
|
+
sequence: (value & 0xffffffffffffffffn).toString(),
|
|
620
|
+
key: (value >> 64n).toString()
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function encodeLegacyUseSignature(permissionId, signature2) {
|
|
624
|
+
const permission = hex(permissionId, "permissionId", 32, 32);
|
|
625
|
+
if (BigInt(permission) === 0n) fail("A permission ID must be nonzero.");
|
|
626
|
+
return concatHex(["0x00", permission, hex(signature2, "signature", 1, 4096)]);
|
|
627
|
+
}
|
|
628
|
+
function encodeSafe7579Execution(calls) {
|
|
629
|
+
const checked = callsChecked(calls);
|
|
630
|
+
const first = checked[0];
|
|
631
|
+
const mode = checked.length === 1 ? SAFE7579_SINGLE_MODE : SAFE7579_BATCH_MODE;
|
|
632
|
+
const executionCalldata = checked.length === 1 ? concatHex([
|
|
633
|
+
first.target,
|
|
634
|
+
toHex3(BigInt(first.value), { size: 32 }),
|
|
635
|
+
first.callData
|
|
636
|
+
]) : encodeAbiParameters2(executionsParameter, [
|
|
637
|
+
checked.map((call) => ({
|
|
638
|
+
...call,
|
|
639
|
+
value: BigInt(call.value)
|
|
640
|
+
}))
|
|
641
|
+
]);
|
|
642
|
+
const encoded = encodeFunctionData({
|
|
643
|
+
abi: SAFE7579_EXECUTION_ABI,
|
|
644
|
+
functionName: "execute",
|
|
645
|
+
args: [mode, executionCalldata]
|
|
646
|
+
});
|
|
647
|
+
if (size(encoded) > MAX_EXECUTION_BYTES)
|
|
648
|
+
fail("The complete execution exceeds the byte limit.");
|
|
649
|
+
return encoded.toLowerCase();
|
|
650
|
+
}
|
|
651
|
+
function decodeSafe7579Execution(callData) {
|
|
652
|
+
const data = hex(callData, "execution", 4, MAX_EXECUTION_BYTES);
|
|
653
|
+
try {
|
|
654
|
+
const decoded = decodeFunctionData({ abi: SAFE7579_EXECUTION_ABI, data });
|
|
655
|
+
const [mode, execution] = decoded.args;
|
|
656
|
+
let calls;
|
|
657
|
+
if (mode === SAFE7579_SINGLE_MODE) {
|
|
658
|
+
if (size(execution) < 52)
|
|
659
|
+
fail("A packed single execution requires target and value.");
|
|
660
|
+
calls = [
|
|
661
|
+
{
|
|
662
|
+
target: getAddress3(sliceHex(execution, 0, 20)),
|
|
663
|
+
value: BigInt(sliceHex(execution, 20, 52)).toString(),
|
|
664
|
+
// viem rejects a slice starting at the end of an exact target/value body.
|
|
665
|
+
callData: size(execution) === 52 ? "0x" : sliceHex(execution, 52)
|
|
666
|
+
}
|
|
667
|
+
];
|
|
668
|
+
} else if (mode === SAFE7579_BATCH_MODE) {
|
|
669
|
+
const [batch] = decodeAbiParameters(executionsParameter, execution);
|
|
670
|
+
if (batch.length < 2)
|
|
671
|
+
fail("Canonical batches contain at least two calls.");
|
|
672
|
+
calls = batch.map((call) => ({ ...call, value: call.value.toString() }));
|
|
673
|
+
} else
|
|
674
|
+
fail("Only default single or batch CALL execution modes are accepted.");
|
|
675
|
+
if (encodeSafe7579Execution(calls) !== data)
|
|
676
|
+
fail("The execution must use its exact canonical ABI encoding.");
|
|
677
|
+
return calls;
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (error instanceof RestError) throw error;
|
|
680
|
+
return fail("The account execution cannot be decoded canonically.");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function assertSafe7579Execution(callData, expectedCalls) {
|
|
684
|
+
const decoded = decodeSafe7579Execution(callData);
|
|
685
|
+
if (encodeSafe7579Execution(expectedCalls) !== callData.toLowerCase())
|
|
686
|
+
fail(
|
|
687
|
+
"The execution differs from the exact approved calls, values, or ordering."
|
|
688
|
+
);
|
|
689
|
+
return decoded;
|
|
690
|
+
}
|
|
691
|
+
var SAFE7579_OWNER_TYPES = {
|
|
692
|
+
EIP712Domain: [
|
|
693
|
+
{ name: "chainId", type: "uint256" },
|
|
694
|
+
{ name: "verifyingContract", type: "address" }
|
|
695
|
+
],
|
|
696
|
+
SafeOp: [
|
|
697
|
+
{ name: "safe", type: "address" },
|
|
698
|
+
{ name: "nonce", type: "uint256" },
|
|
699
|
+
{ name: "initCode", type: "bytes" },
|
|
700
|
+
{ name: "callData", type: "bytes" },
|
|
701
|
+
{ name: "verificationGasLimit", type: "uint128" },
|
|
702
|
+
{ name: "callGasLimit", type: "uint128" },
|
|
703
|
+
{ name: "preVerificationGas", type: "uint256" },
|
|
704
|
+
{ name: "maxPriorityFeePerGas", type: "uint128" },
|
|
705
|
+
{ name: "maxFeePerGas", type: "uint128" },
|
|
706
|
+
{ name: "paymasterAndData", type: "bytes" },
|
|
707
|
+
{ name: "validAfter", type: "uint48" },
|
|
708
|
+
{ name: "validUntil", type: "uint48" },
|
|
709
|
+
{ name: "entryPoint", type: "address" }
|
|
710
|
+
]
|
|
711
|
+
};
|
|
712
|
+
function safe7579OwnerSigningPayload(input) {
|
|
713
|
+
const operation = normalizeUserOperation(input.operation);
|
|
714
|
+
if (BigInt(operation.nonce) >> 96n)
|
|
715
|
+
fail("Owner signing requires a zero-validator nonce.");
|
|
716
|
+
if (!Number.isSafeInteger(input.chainId) || input.chainId <= 0)
|
|
717
|
+
fail("Use the verified execution chain.");
|
|
718
|
+
const validAfter = integer(input.validAfter, "validAfter", false, 48);
|
|
719
|
+
const validUntil = integer(input.validUntil, "validUntil", false, 48);
|
|
720
|
+
if (validUntil <= validAfter)
|
|
721
|
+
fail("Owner operations require a finite ordered validity interval.");
|
|
722
|
+
const packed = packUserOperation(operation);
|
|
723
|
+
const typedData = {
|
|
724
|
+
domain: {
|
|
725
|
+
chainId: BigInt(input.chainId),
|
|
726
|
+
verifyingContract: address(input.safe7579, "safe7579", true)
|
|
727
|
+
},
|
|
728
|
+
types: SAFE7579_OWNER_TYPES,
|
|
729
|
+
primaryType: "SafeOp",
|
|
730
|
+
message: {
|
|
731
|
+
safe: operation.sender,
|
|
732
|
+
nonce: BigInt(operation.nonce),
|
|
733
|
+
initCode: packed.initCode,
|
|
734
|
+
callData: operation.callData,
|
|
735
|
+
verificationGasLimit: BigInt(operation.verificationGasLimit),
|
|
736
|
+
callGasLimit: BigInt(operation.callGasLimit),
|
|
737
|
+
preVerificationGas: BigInt(operation.preVerificationGas),
|
|
738
|
+
maxPriorityFeePerGas: BigInt(operation.maxPriorityFeePerGas),
|
|
739
|
+
maxFeePerGas: BigInt(operation.maxFeePerGas),
|
|
740
|
+
paymasterAndData: packed.paymasterAndData,
|
|
741
|
+
validAfter: Number(validAfter),
|
|
742
|
+
validUntil: Number(validUntil),
|
|
743
|
+
entryPoint: address(input.entryPoint, "entryPoint", true)
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
const digest = hashTypedData3(typedData);
|
|
747
|
+
return {
|
|
748
|
+
scheme: "eip712-safe7579-owner",
|
|
749
|
+
digest,
|
|
750
|
+
// JSON-safe eth_signTypedData_v4 payload; uint values remain exact decimal strings.
|
|
751
|
+
typedData: {
|
|
752
|
+
...typedData,
|
|
753
|
+
domain: { ...typedData.domain, chainId: input.chainId },
|
|
754
|
+
message: {
|
|
755
|
+
...typedData.message,
|
|
756
|
+
nonce: typedData.message.nonce.toString(),
|
|
757
|
+
verificationGasLimit: typedData.message.verificationGasLimit.toString(),
|
|
758
|
+
callGasLimit: typedData.message.callGasLimit.toString(),
|
|
759
|
+
preVerificationGas: typedData.message.preVerificationGas.toString(),
|
|
760
|
+
maxPriorityFeePerGas: typedData.message.maxPriorityFeePerGas.toString(),
|
|
761
|
+
maxFeePerGas: typedData.message.maxFeePerGas.toString(),
|
|
762
|
+
validAfter: validAfter.toString(),
|
|
763
|
+
validUntil: validUntil.toString()
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
validAfter: validAfter.toString(),
|
|
767
|
+
validUntil: validUntil.toString()
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
var SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
771
|
+
function canonicalEoaSignature(value) {
|
|
772
|
+
const signature2 = hex(value, "ECDSA signature", 65, 65);
|
|
773
|
+
const r = BigInt(sliceHex(signature2, 0, 32));
|
|
774
|
+
const s = BigInt(sliceHex(signature2, 32, 64));
|
|
775
|
+
const v = Number(BigInt(sliceHex(signature2, 64)));
|
|
776
|
+
if (r === 0n || r >= SECP256K1_ORDER || s === 0n || s > SECP256K1_ORDER / 2n || v !== 27 && v !== 28)
|
|
777
|
+
fail("Use canonical low-s ECDSA signatures with recovery byte 27 or 28.");
|
|
778
|
+
return signature2;
|
|
779
|
+
}
|
|
780
|
+
function encodeSafe7579OwnerSignature(input) {
|
|
781
|
+
const after = integer(input.validAfter, "validAfter", false, 48);
|
|
782
|
+
const until = integer(input.validUntil, "validUntil", false, 48);
|
|
783
|
+
if (until <= after)
|
|
784
|
+
fail("Owner operations require a finite ordered validity interval.");
|
|
785
|
+
const signatures = hex(input.signatures, "owner signatures", 65, 16 * 65);
|
|
786
|
+
if (size(signatures) % 65 !== 0)
|
|
787
|
+
fail("Owner signatures must be exact 65-byte entries.");
|
|
788
|
+
for (let offset = 0; offset < size(signatures); offset += 65)
|
|
789
|
+
canonicalEoaSignature(sliceHex(signatures, offset, offset + 65));
|
|
790
|
+
return concatHex([
|
|
791
|
+
toHex3(after, { size: 6 }),
|
|
792
|
+
toHex3(until, { size: 6 }),
|
|
793
|
+
signatures
|
|
794
|
+
]);
|
|
795
|
+
}
|
|
796
|
+
function legacySessionSigningPayload(input) {
|
|
797
|
+
const operation = normalizeUserOperation(input.operation);
|
|
798
|
+
const selected = decodeSafe7579Nonce(BigInt(operation.nonce).toString());
|
|
799
|
+
if (selected.validator.toLowerCase() !== address(input.smartSessions, "smartSessions", true).toLowerCase())
|
|
800
|
+
fail("The nonce must select the exact installed SmartSession validator.");
|
|
801
|
+
const permissionId = hex(input.permissionId, "permissionId", 32, 32);
|
|
802
|
+
if (BigInt(permissionId) === 0n) fail("A permission ID must be nonzero.");
|
|
803
|
+
const operationHash = getUserOperationHash(
|
|
804
|
+
operation,
|
|
805
|
+
input.entryPoint,
|
|
806
|
+
input.chainId
|
|
807
|
+
);
|
|
808
|
+
return {
|
|
809
|
+
scheme: "eip191-legacy-ownable-user-operation",
|
|
810
|
+
operationHash,
|
|
811
|
+
digest: hashMessage({ raw: operationHash }),
|
|
812
|
+
message: { raw: operationHash },
|
|
813
|
+
permissionId,
|
|
814
|
+
/** The prefix is an envelope selector, not an additional field in the signed UserOp hash. */
|
|
815
|
+
signaturePrefix: concatHex(["0x00", permissionId])
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/rest/smartAccounts/creation.ts
|
|
820
|
+
import {
|
|
821
|
+
concatHex as concatHex3,
|
|
822
|
+
decodeFunctionData as decodeFunctionData2,
|
|
823
|
+
encodeAbiParameters as encodeAbiParameters4,
|
|
824
|
+
encodeFunctionData as encodeFunctionData2,
|
|
825
|
+
getAddress as getAddress4,
|
|
826
|
+
getContractAddress as getContractAddress2,
|
|
827
|
+
isAddress as isAddress5,
|
|
828
|
+
keccak256 as keccak2564,
|
|
829
|
+
sliceHex as sliceHex2,
|
|
830
|
+
toHex as toHex4,
|
|
831
|
+
zeroAddress
|
|
832
|
+
} from "viem";
|
|
833
|
+
|
|
834
|
+
// src/rest/smartAccounts/passkeyCreation.ts
|
|
835
|
+
import {
|
|
836
|
+
concatHex as concatHex2,
|
|
837
|
+
encodeAbiParameters as encodeAbiParameters3,
|
|
838
|
+
getContractAddress,
|
|
839
|
+
isAddress as isAddress4,
|
|
840
|
+
zeroHash
|
|
841
|
+
} from "viem";
|
|
842
|
+
|
|
843
|
+
// src/rest/client/smartAccounts.ts
|
|
844
|
+
var prefix = "/api/v1";
|
|
845
|
+
var same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
846
|
+
var invalid4 = (message2) => {
|
|
847
|
+
throw new RestClientError("SMART_CLIENT_INVALID", message2);
|
|
848
|
+
};
|
|
849
|
+
var id = (value) => {
|
|
850
|
+
if (!/^(?:0x[0-9a-fA-F]{64}|[A-Za-z0-9_-]{1,128})$/.test(value))
|
|
851
|
+
invalid4("Use the exact identifier returned by the service.");
|
|
852
|
+
return encodeURIComponent(value);
|
|
853
|
+
};
|
|
854
|
+
function smartRequestKey() {
|
|
855
|
+
return `smart-${newRequestNonce().slice(2)}`;
|
|
856
|
+
}
|
|
857
|
+
var SmartAccountClient = class {
|
|
858
|
+
constructor(client) {
|
|
859
|
+
this.client = client;
|
|
860
|
+
}
|
|
861
|
+
client;
|
|
862
|
+
post(path, json, key = smartRequestKey()) {
|
|
863
|
+
return this.client.request({
|
|
864
|
+
method: "POST",
|
|
865
|
+
requestTarget: `${prefix}${path}`,
|
|
866
|
+
json,
|
|
867
|
+
idempotencyKey: key
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
capabilities() {
|
|
871
|
+
return this.client.request({
|
|
872
|
+
requestTarget: `${prefix}/smart-accounts/capabilities`
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
prepareCreation(input, key) {
|
|
876
|
+
return this.post(
|
|
877
|
+
"/smart-accounts/creation-plans",
|
|
878
|
+
input,
|
|
879
|
+
key
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
challenge(input) {
|
|
883
|
+
return this.post(
|
|
884
|
+
"/smart-accounts/binding-challenges",
|
|
885
|
+
input
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
bind(input, key) {
|
|
889
|
+
return this.post(
|
|
890
|
+
"/smart-accounts/bindings",
|
|
891
|
+
input,
|
|
892
|
+
key
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
bindings() {
|
|
896
|
+
return this.client.request({ requestTarget: `${prefix}/smart-accounts/bindings` });
|
|
897
|
+
}
|
|
898
|
+
binding(bindingId) {
|
|
899
|
+
return this.client.request({
|
|
900
|
+
requestTarget: `${prefix}/smart-accounts/bindings/${id(bindingId)}`
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
preparePlan(bindingId, operation, input, key) {
|
|
904
|
+
return this.post(
|
|
905
|
+
`/smart-accounts/bindings/${id(bindingId)}/plans`,
|
|
906
|
+
{ operation, input },
|
|
907
|
+
key
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
prepareSession(input, key) {
|
|
911
|
+
return this.post("/smart-accounts/sessions", input, key);
|
|
912
|
+
}
|
|
913
|
+
session(sessionId) {
|
|
914
|
+
return this.client.request({
|
|
915
|
+
requestTarget: `${prefix}/smart-accounts/sessions/${id(sessionId)}`
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
quota(sessionId) {
|
|
919
|
+
return this.client.request({
|
|
920
|
+
requestTarget: `${prefix}/smart-accounts/sessions/${id(sessionId)}/quota`
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
sessionPlan(sessionId, kind, compiledHash, key) {
|
|
924
|
+
return this.post(
|
|
925
|
+
`/smart-accounts/sessions/${id(sessionId)}/${kind}-plans`,
|
|
926
|
+
{ compiledHash },
|
|
927
|
+
key
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
prepareUserOperation(input, key) {
|
|
931
|
+
return this.post("/user-operations", input, key);
|
|
932
|
+
}
|
|
933
|
+
/** One call: the plan (idempotent on `plan.idempotencyKey`) and the sponsored operation over every
|
|
934
|
+
* one of its steps (idempotent on `key`). When the voucher does not cover the plan Center built,
|
|
935
|
+
* the answer carries the plan alone and `sponsorship: "refused"`, to be sponsored on its own. */
|
|
936
|
+
prepareSponsoredPayment(plan, sponsorAuthorization, key) {
|
|
937
|
+
return this.post(
|
|
938
|
+
"/user-operations",
|
|
939
|
+
{ plan: { bindingId: id(plan.bindingId), operation: plan.operation, input: plan.input, idempotencyKey: plan.idempotencyKey }, sponsorAuthorization },
|
|
940
|
+
key
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
submitUserOperation(operationId, signature2, key) {
|
|
944
|
+
if (!/^0x(?:[0-9a-fA-F]{2}){65,16384}$/.test(signature2))
|
|
945
|
+
invalid4("Use a complete owner or session signature envelope.");
|
|
946
|
+
return this.post(
|
|
947
|
+
`/user-operations/${id(operationId)}/submissions`,
|
|
948
|
+
{ signature: signature2 },
|
|
949
|
+
key
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
/** With `wait`, the answer is held (up to 20 s) until the operation moves past the revision seen. */
|
|
953
|
+
/** Retires an operation nothing has sent, so it is never claimed or sent again; one already claimed comes back with its submission. */
|
|
954
|
+
retireUserOperation(operationId) {
|
|
955
|
+
return this.post(`/user-operations/${id(operationId)}/retirement`, {});
|
|
956
|
+
}
|
|
957
|
+
userOperation(operationId, wait) {
|
|
958
|
+
if (wait && (!Number.isInteger(wait.seconds) || wait.seconds < 1 || wait.seconds > 20 || !Number.isInteger(wait.since) || wait.since < 0))
|
|
959
|
+
invalid4("Wait 1\u201320 seconds past a revision already seen.");
|
|
960
|
+
return this.client.request({
|
|
961
|
+
requestTarget: `${prefix}/user-operations/${id(operationId)}${wait ? `?wait=${wait.seconds}&since=${wait.since}` : ""}`,
|
|
962
|
+
// The server may hold the answer for the wait and then observe once more (a full observation
|
|
963
|
+
// can take several seconds), so the request outlives the ordinary bound by that much.
|
|
964
|
+
...wait ? { timeoutMs: wait.seconds * 1e3 + 15e3 } : {}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
function assertReviewedOperation(record, plan, now = Date.now()) {
|
|
969
|
+
if (record.state !== "prepared" || record.expiresAt <= now || record.createdAt > now + 3e4 || record.planId !== plan.id || !same(record.planCommitment, plan.commitment) || !plan.smartAccount || !same(record.accountBindingId, plan.smartAccount.bindingId) || record.chainId !== plan.smartAccount.chainId || !same(record.operation.sender, plan.smartAccount.address) || record.operation.signature !== "0x" || !record.stepIndexes.length || new Set(record.stepIndexes).size !== record.stepIndexes.length)
|
|
970
|
+
invalid4(
|
|
971
|
+
"Prepare a fresh operation for the exact reviewed plan and wallet."
|
|
972
|
+
);
|
|
973
|
+
const calls = record.stepIndexes.map((index) => {
|
|
974
|
+
const call = Number.isSafeInteger(index) && index >= 0 ? plan.draft.calls[index] : void 0;
|
|
975
|
+
if (!call || call.chainId !== record.chainId)
|
|
976
|
+
return invalid4("The operation selects an invalid plan step.");
|
|
977
|
+
return { target: call.to, value: call.value, callData: call.data };
|
|
978
|
+
});
|
|
979
|
+
assertSafe7579Execution(record.operation.callData, calls);
|
|
980
|
+
if (!same(
|
|
981
|
+
record.operationHash,
|
|
982
|
+
getUserOperationHash(record.operation, record.entryPoint, record.chainId)
|
|
983
|
+
))
|
|
984
|
+
invalid4("The prepared operation hash differs from its contents.");
|
|
985
|
+
}
|
|
986
|
+
async function packOwnerSignatures(input) {
|
|
987
|
+
if (!Number.isSafeInteger(input.threshold) || input.threshold < 1 || input.threshold > 16 || input.signatures.length !== input.threshold)
|
|
988
|
+
invalid4("Collect exactly the wallet's current owner threshold.");
|
|
989
|
+
const order = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
990
|
+
const signatures = await Promise.all(
|
|
991
|
+
input.signatures.map(async (signature2) => {
|
|
992
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature2) || !/^(1b|1c)$/i.test(signature2.slice(-2)))
|
|
993
|
+
invalid4("Use direct EOA typed-data signatures with v27/28.");
|
|
994
|
+
const r = BigInt(`0x${signature2.slice(2, 66)}`), s = BigInt(`0x${signature2.slice(66, 130)}`);
|
|
995
|
+
if (!r || r >= order || !s || s > order / 2n)
|
|
996
|
+
invalid4("Use canonical low-s owner signatures.");
|
|
997
|
+
const address3 = await recoverAddress3({ hash: input.digest, signature: signature2 });
|
|
998
|
+
if (!input.owners.some((owner) => same(owner, address3)))
|
|
999
|
+
invalid4("A signature is not from a current wallet owner.");
|
|
1000
|
+
return { address: address3, signature: signature2 };
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
1003
|
+
signatures.sort((a, b) => BigInt(a.address) < BigInt(b.address) ? -1 : 1);
|
|
1004
|
+
if (new Set(signatures.map(({ address: address3 }) => address3.toLowerCase())).size !== input.threshold)
|
|
1005
|
+
invalid4("Owner signatures must be distinct.");
|
|
1006
|
+
return concatHex4(signatures.map(({ signature: signature2 }) => signature2));
|
|
1007
|
+
}
|
|
1008
|
+
function ownerOperationSigning(input) {
|
|
1009
|
+
const { record, binding: binding2 } = input;
|
|
1010
|
+
if (record.state !== "prepared" || record.expiresAt <= Date.now() || record.session || !same(record.accountBindingId, binding2.id) || !same(record.operation.sender, binding2.wallet.address) || !same(record.accountStateHash, binding2.state.stateHash) || record.chainId !== binding2.wallet.chainId || record.chainId !== input.chainId || !same(record.entryPoint, input.entryPoint) || !same(
|
|
1011
|
+
record.operationHash,
|
|
1012
|
+
getUserOperationHash(record.operation, record.entryPoint, record.chainId)
|
|
1013
|
+
))
|
|
1014
|
+
invalid4(
|
|
1015
|
+
"The owner operation differs from the selected wallet or prepared operation."
|
|
1016
|
+
);
|
|
1017
|
+
const payload = safe7579OwnerSigningPayload({
|
|
1018
|
+
...input,
|
|
1019
|
+
operation: record.operation
|
|
1020
|
+
});
|
|
1021
|
+
if (!same(payload.digest, record.signing.digest) || input.validAfter !== String(Math.floor(record.createdAt / 1e3)) || input.validUntil !== String(Math.floor(record.expiresAt / 1e3)))
|
|
1022
|
+
invalid4("The owner signing window or digest changed.");
|
|
1023
|
+
return payload;
|
|
1024
|
+
}
|
|
1025
|
+
async function ownerOperationSignature(payload, binding2, signatures) {
|
|
1026
|
+
const packed = await packOwnerSignatures({
|
|
1027
|
+
digest: payload.digest,
|
|
1028
|
+
owners: binding2.state.owners,
|
|
1029
|
+
threshold: binding2.state.threshold,
|
|
1030
|
+
signatures
|
|
1031
|
+
});
|
|
1032
|
+
return encodeSafe7579OwnerSignature({
|
|
1033
|
+
validAfter: payload.validAfter,
|
|
1034
|
+
validUntil: payload.validUntil,
|
|
1035
|
+
signatures: packed
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async function signSessionUserOperation(input) {
|
|
1039
|
+
const { record, session, signer } = input, c = session.compiled;
|
|
1040
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1041
|
+
if (session.state !== "active" || !record.session || record.session.id !== session.id || !same(signer.address, c.sessionKey) || !same(record.session.sessionKey, c.sessionKey) || record.session.grantId !== c.grantId || !same(record.session.compiledHash, c.compiledHash) || !same(record.session.permissionId, c.permissionId) || !same(record.accountBindingId, c.bindingId) || record.chainId !== c.chainId || !same(record.operation.sender, c.wallet) || c.validAfter > now || c.validUntil <= now || record.state !== "prepared" || record.expiresAt <= Date.now())
|
|
1042
|
+
invalid4(
|
|
1043
|
+
"This operation is not bound to the active local session key and policy."
|
|
1044
|
+
);
|
|
1045
|
+
const payload = legacySessionSigningPayload({
|
|
1046
|
+
operation: record.operation,
|
|
1047
|
+
chainId: record.chainId,
|
|
1048
|
+
entryPoint: record.entryPoint,
|
|
1049
|
+
smartSessions: c.smartSessions.address,
|
|
1050
|
+
permissionId: c.permissionId
|
|
1051
|
+
});
|
|
1052
|
+
if (!same(payload.operationHash, record.operationHash) || !same(payload.digest, record.signing.digest))
|
|
1053
|
+
invalid4("The operation hash changed before signing.");
|
|
1054
|
+
const signature2 = await signer.signMessage({
|
|
1055
|
+
message: { raw: payload.operationHash }
|
|
1056
|
+
});
|
|
1057
|
+
if (!same(
|
|
1058
|
+
await recoverAddress3({ hash: payload.digest, signature: signature2 }),
|
|
1059
|
+
signer.address
|
|
1060
|
+
))
|
|
1061
|
+
invalid4("The session signer returned the wrong signature.");
|
|
1062
|
+
return encodeLegacyUseSignature(c.permissionId, signature2);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// node_modules/@bananapus/nana-sdk-core/dist/esm/v6/executionErrors.js
|
|
1066
|
+
import { decodeAbiParameters as decodeAbiParameters2, encodeAbiParameters as encodeAbiParameters5, isAddress as isAddress7, keccak256 as keccak2566, maxUint256 } from "viem";
|
|
1067
|
+
var REVERT_DATA = /^0x[0-9a-f]+$/i;
|
|
1068
|
+
function collectEvidence(value, evidence = { payloads: [], decoded: [] }, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
1069
|
+
if (depth > 8 || value === null || value === void 0 || seen.has(value)) {
|
|
1070
|
+
return evidence;
|
|
1071
|
+
}
|
|
1072
|
+
if (typeof value === "string") {
|
|
1073
|
+
if (REVERT_DATA.test(value))
|
|
1074
|
+
evidence.payloads.push(value);
|
|
1075
|
+
return evidence;
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof value !== "object")
|
|
1078
|
+
return evidence;
|
|
1079
|
+
seen.add(value);
|
|
1080
|
+
const record = value;
|
|
1081
|
+
if (typeof record.errorName === "string") {
|
|
1082
|
+
evidence.decoded.push({
|
|
1083
|
+
errorName: record.errorName,
|
|
1084
|
+
abiItem: record.abiItem,
|
|
1085
|
+
args: record.args
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
for (const item of [record.raw, record.data, record.cause, record.error]) {
|
|
1089
|
+
collectEvidence(item, evidence, seen, depth + 1);
|
|
1090
|
+
}
|
|
1091
|
+
return evidence;
|
|
1092
|
+
}
|
|
1093
|
+
var PERMIT_MESSAGE = "The signed token permit covers less than the payment, so sign it again for the full amount.";
|
|
1094
|
+
var SHORTFALL_MESSAGE = "This payment would get fewer tokens than its minimum, so try again.";
|
|
1095
|
+
var ROUTE_FAILED = "0xa4ceb15e";
|
|
1096
|
+
var NO_TOKENS_MESSAGE = "This payment would return no tokens.";
|
|
1097
|
+
var UNEXPECTED_ETH_MESSAGE = "A payment in this token cannot also send ETH.";
|
|
1098
|
+
var KNOWN_REVERTS = [
|
|
1099
|
+
{
|
|
1100
|
+
reason: "BUYBACK_SLIPPAGE_EXCEEDED",
|
|
1101
|
+
name: "JBBuybackHook_SpecifiedSlippageExceeded",
|
|
1102
|
+
selector: "0xe2d708a9",
|
|
1103
|
+
params: ["uint256", "uint256"],
|
|
1104
|
+
shortfall: [0, 1],
|
|
1105
|
+
adjustable: "minimum",
|
|
1106
|
+
message: "The price moved and this payment would get fewer tokens than your minimum, so lower your minimum or try again.",
|
|
1107
|
+
// The hook checks only the part it buys with; the rest can still mint.
|
|
1108
|
+
zeroMessage: "The price moved and the part of this payment that buys tokens would get none, so try again."
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
reason: "TERMINAL_UNDER_MIN",
|
|
1112
|
+
name: "JBMultiTerminal_UnderMin",
|
|
1113
|
+
selector: "0x6b2bb382",
|
|
1114
|
+
params: ["uint256", "uint256"],
|
|
1115
|
+
shortfall: [0, 1],
|
|
1116
|
+
adjustable: "minimum",
|
|
1117
|
+
message: "This payment would get fewer tokens than your minimum, so lower your minimum or try again.",
|
|
1118
|
+
zeroMessage: NO_TOKENS_MESSAGE
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
reason: "BUYBACK_DERIVED_FLOOR_NOT_MET",
|
|
1122
|
+
name: "JBBuybackHook_DerivedFloorNotMet",
|
|
1123
|
+
selector: "0xe490a239",
|
|
1124
|
+
params: ["uint256", "uint256"],
|
|
1125
|
+
shortfall: [0, 1],
|
|
1126
|
+
message: "The token's price moved too fast to buy safely, so wait a few minutes and try again."
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
reason: "ROUTER_SLIPPAGE_EXCEEDED",
|
|
1130
|
+
name: "JBRouterTerminal_SlippageExceeded",
|
|
1131
|
+
selector: "0x1dbd2ab5",
|
|
1132
|
+
params: ["uint256", "uint256"],
|
|
1133
|
+
shortfall: [0, 1],
|
|
1134
|
+
message: "The price moved while converting your payment, so try again.",
|
|
1135
|
+
zeroMessage: "Converting this payment would return nothing."
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
reason: "BUYBACK_SWAP_AMOUNT_TOO_HIGH",
|
|
1139
|
+
name: "JBBuybackHook_InsufficientPayAmount",
|
|
1140
|
+
selector: "0xcc8704dd",
|
|
1141
|
+
params: ["uint256", "uint256"],
|
|
1142
|
+
message: "The amount set aside to buy tokens is more than the payment."
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
reason: "ROUTER_NO_ROUTE",
|
|
1146
|
+
name: "JBRouterTerminal_NoRouteFound",
|
|
1147
|
+
selector: "0x52027b4a",
|
|
1148
|
+
params: ["uint256", "address"],
|
|
1149
|
+
message: "This token cannot be converted into one the project accepts."
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
reason: "ROUTER_NO_ROUTE",
|
|
1153
|
+
name: "JBRouterTerminal_NoPoolFound",
|
|
1154
|
+
selector: "0x04400f4a",
|
|
1155
|
+
params: ["address", "address"],
|
|
1156
|
+
message: "This token cannot be converted into one the project accepts."
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
reason: "TOKEN_NOT_ACCEPTED",
|
|
1160
|
+
name: "JBMultiTerminal_TokenNotAccepted",
|
|
1161
|
+
selector: "0xecb6c1d6",
|
|
1162
|
+
params: ["address"],
|
|
1163
|
+
message: "This project does not accept this token."
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
reason: "PAYMENTS_PAUSED",
|
|
1167
|
+
name: "JBTerminalStore_RulesetPaymentPaused",
|
|
1168
|
+
selector: "0x8dc14446",
|
|
1169
|
+
params: ["uint256", "uint256"],
|
|
1170
|
+
message: "This project is not accepting payments right now."
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
reason: "NO_RULESET",
|
|
1174
|
+
name: "JBTerminalStore_RulesetNotFound",
|
|
1175
|
+
selector: "0x453ebd8f",
|
|
1176
|
+
params: ["uint256"],
|
|
1177
|
+
message: "This project is not set up to accept payments yet."
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
reason: "ALLOWANCE_TOO_LOW",
|
|
1181
|
+
name: "JBMultiTerminal_PermitAllowanceNotEnough",
|
|
1182
|
+
selector: "0xc7e994ba",
|
|
1183
|
+
params: ["uint256", "uint256"],
|
|
1184
|
+
message: PERMIT_MESSAGE
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
reason: "ALLOWANCE_TOO_LOW",
|
|
1188
|
+
name: "ERC20InsufficientAllowance",
|
|
1189
|
+
selector: "0xfb8f41b2",
|
|
1190
|
+
params: ["address", "uint256", "uint256"],
|
|
1191
|
+
message: "The token approval is less than the payment, so approve the full amount and try again."
|
|
1192
|
+
},
|
|
1193
|
+
...[
|
|
1194
|
+
["JBRouterTerminal_PermitAllowanceNotEnough", "0x7686b38a"],
|
|
1195
|
+
["JBRouterTerminalRegistry_PermitAllowanceNotEnough", "0x3a521783"],
|
|
1196
|
+
["JBRouterTerminalGateway_PermitAllowanceNotEnough", "0x0f420d6e"]
|
|
1197
|
+
].map(([name, selector]) => ({
|
|
1198
|
+
reason: "ALLOWANCE_TOO_LOW",
|
|
1199
|
+
name,
|
|
1200
|
+
selector,
|
|
1201
|
+
params: ["uint256", "uint256"],
|
|
1202
|
+
message: PERMIT_MESSAGE
|
|
1203
|
+
})),
|
|
1204
|
+
{
|
|
1205
|
+
reason: "ALLOWANCE_TOO_LOW",
|
|
1206
|
+
name: "InsufficientAllowance",
|
|
1207
|
+
selector: "0xf96fb071",
|
|
1208
|
+
params: ["uint256"],
|
|
1209
|
+
message: "The Permit2 allowance is less than the payment, so approve the full amount again."
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
reason: "ALLOWANCE_EXPIRED",
|
|
1213
|
+
name: "AllowanceExpired",
|
|
1214
|
+
selector: "0xd81b2f2e",
|
|
1215
|
+
params: ["uint256"],
|
|
1216
|
+
message: "The Permit2 approval has expired, so approve it again and retry."
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
reason: "BALANCE_TOO_LOW",
|
|
1220
|
+
name: "ERC20InsufficientBalance",
|
|
1221
|
+
selector: "0xe450d38c",
|
|
1222
|
+
params: ["address", "uint256", "uint256"],
|
|
1223
|
+
message: "Your token balance is less than the payment."
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
reason: "UNEXPECTED_ETH",
|
|
1227
|
+
name: "JBMultiTerminal_NoMsgValueAllowed",
|
|
1228
|
+
selector: "0x3ad385fd",
|
|
1229
|
+
params: ["uint256"],
|
|
1230
|
+
message: UNEXPECTED_ETH_MESSAGE
|
|
1231
|
+
},
|
|
1232
|
+
...[
|
|
1233
|
+
["JBRouterTerminal_NoMsgValueAllowed", "0x90b2b3e2"],
|
|
1234
|
+
["JBRouterTerminalRegistry_NoMsgValueAllowed", "0x0fa1a882"],
|
|
1235
|
+
["JBRouterTerminalGateway_NoMsgValueAllowed", "0x49ce25f0"]
|
|
1236
|
+
].map(([name, selector]) => ({
|
|
1237
|
+
reason: "UNEXPECTED_ETH",
|
|
1238
|
+
name,
|
|
1239
|
+
selector,
|
|
1240
|
+
params: ["uint256"],
|
|
1241
|
+
message: UNEXPECTED_ETH_MESSAGE
|
|
1242
|
+
})),
|
|
1243
|
+
{
|
|
1244
|
+
reason: "NFT_SOLD_OUT",
|
|
1245
|
+
name: "JB721TiersHookStore_InsufficientSupplyRemaining",
|
|
1246
|
+
selector: "0x90a08e24",
|
|
1247
|
+
params: ["uint256"],
|
|
1248
|
+
message: "One of the NFTs you picked is sold out."
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
reason: "NFT_UNAVAILABLE",
|
|
1252
|
+
name: "JB721TiersHookStore_TierRemoved",
|
|
1253
|
+
selector: "0x8145a027",
|
|
1254
|
+
params: ["uint256"],
|
|
1255
|
+
message: "One of the NFTs you picked is no longer available."
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
reason: "NFT_UNAVAILABLE",
|
|
1259
|
+
name: "JB721TiersHookStore_UnrecognizedTier",
|
|
1260
|
+
selector: "0xa6fe1eee",
|
|
1261
|
+
params: ["uint256"],
|
|
1262
|
+
message: "One of the NFTs you picked is no longer available."
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
reason: "NFT_PRICE_ABOVE_PAYMENT",
|
|
1266
|
+
name: "JB721TiersHookStore_PriceExceedsAmount",
|
|
1267
|
+
selector: "0xee890b46",
|
|
1268
|
+
params: ["uint256", "uint256"],
|
|
1269
|
+
message: "The payment is less than the price of the NFTs you picked."
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
reason: "NFT_OVERSPENDING",
|
|
1273
|
+
name: "JB721TiersHook_Overspending",
|
|
1274
|
+
selector: "0x42b9988e",
|
|
1275
|
+
params: ["uint256"],
|
|
1276
|
+
message: "This payment, with any NFT credits you hold, is more than the NFTs you picked cost, and the extra is not allowed."
|
|
1277
|
+
}
|
|
1278
|
+
];
|
|
1279
|
+
function validArgs(known, args) {
|
|
1280
|
+
if (!Array.isArray(args) || args.length !== known.params.length)
|
|
1281
|
+
return;
|
|
1282
|
+
const typed = known.params.every((type, i) => type === "address" ? typeof args[i] === "string" && isAddress7(args[i], { strict: false }) : typeof args[i] === "bigint" && args[i] >= 0n && args[i] <= maxUint256);
|
|
1283
|
+
if (!typed)
|
|
1284
|
+
return;
|
|
1285
|
+
if (known.shortfall && args[known.shortfall[0]] >= args[known.shortfall[1]]) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
return args;
|
|
1289
|
+
}
|
|
1290
|
+
function matchKnownRevert(error, reasons) {
|
|
1291
|
+
const known = reasons ? KNOWN_REVERTS.filter((k) => reasons.includes(k.reason)) : KNOWN_REVERTS;
|
|
1292
|
+
const { payloads, decoded } = collectEvidence(error);
|
|
1293
|
+
for (const hex2 of payloads) {
|
|
1294
|
+
const lower = hex2.toLowerCase();
|
|
1295
|
+
if (lower.startsWith(ROUTE_FAILED) && lower.length === 10 + 64) {
|
|
1296
|
+
const errorHash = `0x${lower.slice(10)}`;
|
|
1297
|
+
const wrapped = known.find((k) => keccak2566(k.selector) === errorHash);
|
|
1298
|
+
if (wrapped)
|
|
1299
|
+
return { known: wrapped };
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const match = known.find((k) => lower.startsWith(k.selector) && lower.length === 10 + 64 * k.params.length);
|
|
1303
|
+
if (!match)
|
|
1304
|
+
continue;
|
|
1305
|
+
const params = match.params.map((type) => ({ type }));
|
|
1306
|
+
const data = `0x${lower.slice(10)}`;
|
|
1307
|
+
const args = validArgs(match, decodeAbiParameters2(params, data));
|
|
1308
|
+
if (args && encodeAbiParameters5(params, args) === data) {
|
|
1309
|
+
return { known: match, args };
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (payloads.length)
|
|
1313
|
+
return null;
|
|
1314
|
+
for (const { errorName, abiItem, args } of decoded) {
|
|
1315
|
+
const item = abiItem;
|
|
1316
|
+
const match = known.find((k) => errorName === k.name && item?.type === "error" && item.name === k.name && Array.isArray(item.inputs) && item.inputs.length === k.params.length && k.params.every((type, i) => item.inputs?.[i]?.type === type));
|
|
1317
|
+
const valid = match && validArgs(match, args);
|
|
1318
|
+
if (match && valid)
|
|
1319
|
+
return { known: match, args: valid };
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
function decodePayFailure(error) {
|
|
1324
|
+
const matched = matchKnownRevert(error);
|
|
1325
|
+
if (!matched)
|
|
1326
|
+
return null;
|
|
1327
|
+
const { known, args } = matched;
|
|
1328
|
+
if (!known.shortfall)
|
|
1329
|
+
return { reason: known.reason, message: known.message };
|
|
1330
|
+
if (!args) {
|
|
1331
|
+
return {
|
|
1332
|
+
reason: known.reason,
|
|
1333
|
+
message: known.adjustable ? SHORTFALL_MESSAGE : known.message
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const received = args[known.shortfall[0]];
|
|
1337
|
+
const minimum = args[known.shortfall[1]];
|
|
1338
|
+
const zeroFill = received === 0n;
|
|
1339
|
+
return {
|
|
1340
|
+
reason: known.reason,
|
|
1341
|
+
received,
|
|
1342
|
+
minimum,
|
|
1343
|
+
zeroFill,
|
|
1344
|
+
...!zeroFill && known.adjustable ? { adjustable: known.adjustable } : {},
|
|
1345
|
+
message: zeroFill && known.zeroMessage || known.message
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/rest/client/payFailure.ts
|
|
1350
|
+
import { encodeAbiParameters as encodeAbiParameters6, keccak256 as keccak2567, toFunctionSelector } from "viem";
|
|
1351
|
+
var amount = (value) => value === void 0 || typeof value === "string" && /^(0|[1-9][0-9]{0,77})$/.test(value) && BigInt(value) < 2n ** 256n;
|
|
1352
|
+
var shortfallReverts = [
|
|
1353
|
+
"JBMultiTerminal_UnderMin(uint256,uint256)",
|
|
1354
|
+
"JBBuybackHook_SpecifiedSlippageExceeded(uint256,uint256)",
|
|
1355
|
+
"JBBuybackHook_DerivedFloorNotMet(uint256,uint256)",
|
|
1356
|
+
"JBRouterTerminal_SlippageExceeded(uint256,uint256)"
|
|
1357
|
+
];
|
|
1358
|
+
var payFailureReverts = {
|
|
1359
|
+
TERMINAL_UNDER_MIN: ["JBMultiTerminal_UnderMin(uint256,uint256)"],
|
|
1360
|
+
BUYBACK_SLIPPAGE_EXCEEDED: ["JBBuybackHook_SpecifiedSlippageExceeded(uint256,uint256)"],
|
|
1361
|
+
BUYBACK_DERIVED_FLOOR_NOT_MET: ["JBBuybackHook_DerivedFloorNotMet(uint256,uint256)"],
|
|
1362
|
+
ROUTER_SLIPPAGE_EXCEEDED: ["JBRouterTerminal_SlippageExceeded(uint256,uint256)"],
|
|
1363
|
+
BUYBACK_SWAP_AMOUNT_TOO_HIGH: ["JBBuybackHook_InsufficientPayAmount(uint256,uint256)"],
|
|
1364
|
+
ROUTER_NO_ROUTE: ["JBRouterTerminal_NoRouteFound(uint256,address)", "JBRouterTerminal_NoPoolFound(address,address)"],
|
|
1365
|
+
TOKEN_NOT_ACCEPTED: ["JBMultiTerminal_TokenNotAccepted(address)"],
|
|
1366
|
+
PAYMENTS_PAUSED: ["JBTerminalStore_RulesetPaymentPaused(uint256,uint256)"],
|
|
1367
|
+
NO_RULESET: ["JBTerminalStore_RulesetNotFound(uint256)"],
|
|
1368
|
+
ALLOWANCE_TOO_LOW: [
|
|
1369
|
+
"JBMultiTerminal_PermitAllowanceNotEnough(uint256,uint256)",
|
|
1370
|
+
"ERC20InsufficientAllowance(address,uint256,uint256)",
|
|
1371
|
+
"InsufficientAllowance(uint256)"
|
|
1372
|
+
],
|
|
1373
|
+
ALLOWANCE_EXPIRED: ["AllowanceExpired(uint256)"],
|
|
1374
|
+
BALANCE_TOO_LOW: ["ERC20InsufficientBalance(address,uint256,uint256)"],
|
|
1375
|
+
UNEXPECTED_ETH: ["JBMultiTerminal_NoMsgValueAllowed(uint256)"],
|
|
1376
|
+
NFT_SOLD_OUT: ["JB721TiersHookStore_InsufficientSupplyRemaining(uint256)"],
|
|
1377
|
+
NFT_UNAVAILABLE: ["JB721TiersHookStore_TierRemoved(uint256)", "JB721TiersHookStore_UnrecognizedTier(uint256)"],
|
|
1378
|
+
NFT_PRICE_ABOVE_PAYMENT: ["JB721TiersHookStore_PriceExceedsAmount(uint256,uint256)"],
|
|
1379
|
+
NFT_OVERSPENDING: ["JB721TiersHook_Overspending(uint256)"]
|
|
1380
|
+
};
|
|
1381
|
+
var ROUTE_FAILED2 = "0xa4ceb15e";
|
|
1382
|
+
function sdkPayFailures(reason, received, minimum) {
|
|
1383
|
+
return payFailureReverts[reason].map((signature2) => {
|
|
1384
|
+
const types2 = signature2.slice(signature2.indexOf("(") + 1, -1).split(",").map((type) => ({ type }));
|
|
1385
|
+
const selector = toFunctionSelector(signature2);
|
|
1386
|
+
const data = shortfallReverts.includes(signature2) ? received === void 0 || minimum === void 0 ? `${ROUTE_FAILED2}${keccak2567(selector).slice(2)}` : `${selector}${encodeAbiParameters6(types2, [received, minimum]).slice(2)}` : `${selector}${encodeAbiParameters6(types2, types2.map(({ type }) => type === "address" ? `0x${"11".repeat(20)}` : 1n)).slice(2)}`;
|
|
1387
|
+
try {
|
|
1388
|
+
return decodePayFailure(data);
|
|
1389
|
+
} catch {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
function payFailureOf(value) {
|
|
1395
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1396
|
+
const { reason, received, minimum, message: message2, ...rest } = value;
|
|
1397
|
+
if (Object.keys(rest).length || typeof reason !== "string" || !Object.hasOwn(payFailureReverts, reason) || typeof message2 !== "string" || !amount(received) || !amount(minimum)) return void 0;
|
|
1398
|
+
const settled = received === void 0 ? void 0 : BigInt(received);
|
|
1399
|
+
const floor = minimum === void 0 ? void 0 : BigInt(minimum);
|
|
1400
|
+
return sdkPayFailures(reason, settled, floor).find((failure) => failure && failure.reason === reason && failure.message === message2 && failure.received === settled && failure.minimum === floor) ?? void 0;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/rest/client/center.ts
|
|
1404
|
+
import { keccak256 as keccak2568, parseTransaction, recoverTransactionAddress } from "viem";
|
|
1405
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
1406
|
+
|
|
1407
|
+
// src/rest/client/connection.ts
|
|
1408
|
+
import { isAddress as isAddress8 } from "viem";
|
|
1409
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
1410
|
+
function parseConnection(value) {
|
|
1411
|
+
const invalid7 = () => {
|
|
1412
|
+
throw new RestClientError("INVALID_CONNECTION", "Choose the connection file downloaded after registering your bot.");
|
|
1413
|
+
};
|
|
1414
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid7();
|
|
1415
|
+
const v = value;
|
|
1416
|
+
if (Object.keys(v).some((key) => !["format", "audience", "accountId", "grantId", "botAddress", "privateKey", "scopes", "expiresAt"].includes(key)) || v.format !== "juicebox-center-connection-v1" || typeof v.audience !== "string" || typeof v.accountId !== "string" || typeof v.grantId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(v.grantId) || typeof v.botAddress !== "string" || !isAddress8(v.botAddress) || typeof v.privateKey !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(v.privateKey) || !isCanonicalGrantScopes(v.scopes) || !Number.isSafeInteger(v.expiresAt) || Number(v.expiresAt) < 1) return invalid7();
|
|
1417
|
+
try {
|
|
1418
|
+
clientAudience(v.audience);
|
|
1419
|
+
parseAccountId(v.accountId);
|
|
1420
|
+
if (privateKeyToAccount(v.privateKey).address.toLowerCase() !== v.botAddress.toLowerCase()) return invalid7();
|
|
1421
|
+
} catch {
|
|
1422
|
+
return invalid7();
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
format: "juicebox-center-connection-v1",
|
|
1426
|
+
audience: v.audience,
|
|
1427
|
+
accountId: v.accountId,
|
|
1428
|
+
grantId: v.grantId,
|
|
1429
|
+
botAddress: v.botAddress,
|
|
1430
|
+
privateKey: v.privateKey,
|
|
1431
|
+
scopes: [...v.scopes],
|
|
1432
|
+
expiresAt: Number(v.expiresAt)
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/rest/client/center.ts
|
|
1437
|
+
var id2 = (value) => {
|
|
1438
|
+
if (!/^[A-Za-z0-9._:-]{1,200}$/.test(value)) throw new RestClientError("INVALID_INPUT", "Use the identifier returned by Center.");
|
|
1439
|
+
return encodeURIComponent(value);
|
|
1440
|
+
};
|
|
1441
|
+
var CenterClient = class _CenterClient {
|
|
1442
|
+
constructor(options) {
|
|
1443
|
+
this.options = options;
|
|
1444
|
+
this.client = new SignedRestClient2({ payFailure: payFailureOf, ...options });
|
|
1445
|
+
}
|
|
1446
|
+
options;
|
|
1447
|
+
client;
|
|
1448
|
+
request(options) {
|
|
1449
|
+
return this.client.request(options);
|
|
1450
|
+
}
|
|
1451
|
+
/** Authorizes one exact GET for a relay. Send the returned URL and headers unchanged; this method does not send it. */
|
|
1452
|
+
authorizeRead(requestTarget) {
|
|
1453
|
+
return prepareSignedRequest(this.options, { method: "GET", requestTarget });
|
|
1454
|
+
}
|
|
1455
|
+
/** Uses this connection's existing API identity and grant on every execution network. */
|
|
1456
|
+
smartAccounts() {
|
|
1457
|
+
return new SmartAccountClient(this.client);
|
|
1458
|
+
}
|
|
1459
|
+
/** Signs locally with the bot key after checking the exact plan and activated permission. Does not submit. */
|
|
1460
|
+
async signSessionOperation(input) {
|
|
1461
|
+
const reviewed = structuredClone(input), signer = this.options.signer;
|
|
1462
|
+
if (!this.options.grantId || reviewed.session.compiled.grantId !== this.options.grantId || reviewed.session.compiled.ownerAccountId !== this.options.accountId)
|
|
1463
|
+
throw new RestClientError("SESSION_CONNECTION_MISMATCH", "Use the bot connection approved for this wallet permission.");
|
|
1464
|
+
if (!signer.signMessage)
|
|
1465
|
+
throw new RestClientError("SESSION_SIGNER_REQUIRED", "The bot signer must support signing a raw message hash.");
|
|
1466
|
+
assertReviewedOperation(reviewed.operation, reviewed.plan);
|
|
1467
|
+
return signSessionUserOperation({
|
|
1468
|
+
record: reviewed.operation,
|
|
1469
|
+
session: reviewed.session,
|
|
1470
|
+
signer: { address: signer.address, signMessage: (value) => signer.signMessage(value) }
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
static forOwner(options) {
|
|
1474
|
+
const { chainId, ...config } = options;
|
|
1475
|
+
return new _CenterClient({ ...config, accountId: accountIdFor(options.signer.address, chainId) });
|
|
1476
|
+
}
|
|
1477
|
+
static fromConnection(value, options = {}) {
|
|
1478
|
+
const connection = parseConnection(value);
|
|
1479
|
+
if (connection.expiresAt <= (options.now?.() ?? Math.floor(Date.now() / 1e3)))
|
|
1480
|
+
throw new RestClientError("CONNECTION_EXPIRED", "This bot connection expired. Register a new grant on Accounts.");
|
|
1481
|
+
return new _CenterClient({
|
|
1482
|
+
...options,
|
|
1483
|
+
audience: connection.audience,
|
|
1484
|
+
accountId: connection.accountId,
|
|
1485
|
+
grantId: connection.grantId,
|
|
1486
|
+
signer: privateKeyToAccount2(connection.privateKey)
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
account() {
|
|
1490
|
+
return this.request({ requestTarget: "/api/v1/accounts/me" });
|
|
1491
|
+
}
|
|
1492
|
+
enroll() {
|
|
1493
|
+
return this.request({ method: "POST", requestTarget: "/api/v1/accounts/enroll", json: {} });
|
|
1494
|
+
}
|
|
1495
|
+
bots() {
|
|
1496
|
+
return this.request({ requestTarget: "/api/v1/accounts/me/bots" });
|
|
1497
|
+
}
|
|
1498
|
+
revokeBot(grantId) {
|
|
1499
|
+
return this.request({ method: "DELETE", requestTarget: `/api/v1/accounts/me/bots/${id2(grantId)}` });
|
|
1500
|
+
}
|
|
1501
|
+
async registerBot(input) {
|
|
1502
|
+
const proof = await createBotRegistration(this.options.audience, {
|
|
1503
|
+
accountId: this.options.accountId,
|
|
1504
|
+
botAddress: input.signer.address,
|
|
1505
|
+
scopes: input.scopes,
|
|
1506
|
+
label: input.label,
|
|
1507
|
+
expiresAt: input.expiresAt,
|
|
1508
|
+
ownerRequestNonce: newRequestNonce()
|
|
1509
|
+
}, input.signer);
|
|
1510
|
+
const { bot } = await this.request({ method: "POST", requestTarget: "/api/v1/accounts/me/bots", json: proof.registration, nonce: proof.ownerRequestNonce });
|
|
1511
|
+
if (bot.accountId !== this.options.accountId || bot.botAddress.toLowerCase() !== input.signer.address.toLowerCase() || bot.expiresAt !== input.expiresAt || bot.revokedAt !== null || JSON.stringify(bot.scopes) !== JSON.stringify(input.scopes))
|
|
1512
|
+
throw new RestClientError("INVALID_RESPONSE", "Registration did not match the requested API access.");
|
|
1513
|
+
return { bot, client: new _CenterClient({ ...this.options, signer: input.signer, grantId: bot.id }) };
|
|
1514
|
+
}
|
|
1515
|
+
capabilities() {
|
|
1516
|
+
return readPublicRestJson(this.options.audience, "/api/v1/capabilities", this.options.fetch);
|
|
1517
|
+
}
|
|
1518
|
+
prepare(input, idempotencyKey) {
|
|
1519
|
+
return this.request({ method: "POST", requestTarget: "/api/v1/plans", json: input, idempotencyKey });
|
|
1520
|
+
}
|
|
1521
|
+
plan(planId, options = {}) {
|
|
1522
|
+
return this.request({ requestTarget: `/api/v1/plans/${id2(planId)}${options.refresh ? "?refresh=true" : ""}` });
|
|
1523
|
+
}
|
|
1524
|
+
simulate(planId, stepIndex) {
|
|
1525
|
+
this.step(stepIndex);
|
|
1526
|
+
return this.request({ requestTarget: `/api/v1/plans/${id2(planId)}/steps/${stepIndex}/simulation` });
|
|
1527
|
+
}
|
|
1528
|
+
/** Call after the owner reviews the plan and the transaction wallet signs the exact bytes. */
|
|
1529
|
+
async approveTransaction(input, owner) {
|
|
1530
|
+
this.step(input.stepIndex);
|
|
1531
|
+
if (!input.plan.draft.calls[input.stepIndex] || input.plan.expiresAt <= (this.options.now?.() ?? Math.floor(Date.now() / 1e3)) * 1e3)
|
|
1532
|
+
throw new RestClientError("PLAN_NOT_READY", "Review a current plan and choose one of its steps.");
|
|
1533
|
+
input = structuredClone(input);
|
|
1534
|
+
const call = input.plan.draft.calls[input.stepIndex];
|
|
1535
|
+
try {
|
|
1536
|
+
const transaction = parseTransaction(input.rawSignedTransaction);
|
|
1537
|
+
const sender = await recoverTransactionAddress({ serializedTransaction: input.rawSignedTransaction });
|
|
1538
|
+
if (transaction.chainId !== call.chainId || transaction.to?.toLowerCase() !== call.to.toLowerCase() || (transaction.data ?? "0x").toLowerCase() !== call.data.toLowerCase() || (transaction.value ?? 0n) !== BigInt(call.value) || sender.toLowerCase() !== input.plan.account.toLowerCase() || !["legacy", "eip2930", "eip1559"].includes(transaction.type ?? "legacy")) throw new Error("Mismatch");
|
|
1539
|
+
} catch {
|
|
1540
|
+
throw new RestClientError("SIGNED_TRANSACTION_MISMATCH", "The wallet-signed transaction does not match this reviewed plan step.");
|
|
1541
|
+
}
|
|
1542
|
+
const result = { planId: input.plan.id, stepIndex: input.stepIndex, rawSignedTransaction: input.rawSignedTransaction };
|
|
1543
|
+
if (!this.options.grantId) return result;
|
|
1544
|
+
const claims2 = {
|
|
1545
|
+
...this.approvalWindow(owner),
|
|
1546
|
+
planId: input.plan.id,
|
|
1547
|
+
commitment: input.plan.commitment,
|
|
1548
|
+
stepIndex: input.stepIndex,
|
|
1549
|
+
transactionHash: keccak2568(input.rawSignedTransaction)
|
|
1550
|
+
};
|
|
1551
|
+
return { ...result, ownerApproval: { ...claims2, signature: await owner.signTypedData(buildTransactionApprovalTypedData(this.options.audience, claims2)) } };
|
|
1552
|
+
}
|
|
1553
|
+
submitTransaction(input, idempotencyKey) {
|
|
1554
|
+
this.step(input.stepIndex);
|
|
1555
|
+
const { planId, stepIndex, ...json } = input;
|
|
1556
|
+
return this.request({ method: "POST", requestTarget: `/api/v1/plans/${id2(planId)}/steps/${stepIndex}/submissions`, json, idempotencyKey });
|
|
1557
|
+
}
|
|
1558
|
+
preparePrepaid(planId, stepIndexes, idempotencyKey) {
|
|
1559
|
+
return this.request({ method: "POST", requestTarget: "/api/v1/sponsorships", json: { planId, ...stepIndexes ? { stepIndexes } : {} }, idempotencyKey });
|
|
1560
|
+
}
|
|
1561
|
+
prepaid(sponsorshipId, options = {}) {
|
|
1562
|
+
return this.request({ requestTarget: `/api/v1/sponsorships/${id2(sponsorshipId)}${options.refresh ? "?refresh=true" : ""}` });
|
|
1563
|
+
}
|
|
1564
|
+
async approvePrepaid(preparation, signatures, owner) {
|
|
1565
|
+
if (signatures.length !== preparation.authorizations.length || preparation.publicationExpiresAt <= (this.options.now?.() ?? Math.floor(Date.now() / 1e3)) * 1e3)
|
|
1566
|
+
throw new RestClientError("PLAN_NOT_READY", "Review a current preparation and sign each returned authorization in order.");
|
|
1567
|
+
const result = { sponsorshipId: preparation.id, signatures: [...signatures] };
|
|
1568
|
+
if (!this.options.grantId) return result;
|
|
1569
|
+
const claims2 = {
|
|
1570
|
+
...this.approvalWindow(owner),
|
|
1571
|
+
sponsorshipId: preparation.id,
|
|
1572
|
+
commitment: preparation.commitment,
|
|
1573
|
+
submissionHash: sponsorshipSubmissionHash(preparation.commitment, signatures)
|
|
1574
|
+
};
|
|
1575
|
+
return { ...result, ownerApproval: { ...claims2, signature: await owner.signTypedData(buildSponsorshipApprovalTypedData(this.options.audience, claims2)) } };
|
|
1576
|
+
}
|
|
1577
|
+
submitPrepaid(input, idempotencyKey) {
|
|
1578
|
+
const { sponsorshipId, ...json } = input;
|
|
1579
|
+
return this.request({ method: "POST", requestTarget: `/api/v1/sponsorships/${id2(sponsorshipId)}/submissions`, json, idempotencyKey });
|
|
1580
|
+
}
|
|
1581
|
+
prepareFunding(sponsorshipId, chainId, payer, idempotencyKey) {
|
|
1582
|
+
return this.request({ method: "POST", requestTarget: `/api/v1/sponsorships/${id2(sponsorshipId)}/funding-plans`, json: { chainId, payer }, idempotencyKey });
|
|
1583
|
+
}
|
|
1584
|
+
step(index) {
|
|
1585
|
+
if (!Number.isInteger(index) || index < 0 || index > 31) throw new RestClientError("INVALID_INPUT", "Choose a plan step from 0 through 31.");
|
|
1586
|
+
}
|
|
1587
|
+
approvalWindow(owner) {
|
|
1588
|
+
if (parseAccountId(this.options.accountId).ownerAddress.toLowerCase() !== owner.address.toLowerCase())
|
|
1589
|
+
throw new RestClientError("OWNER_MISMATCH", "The account owner must approve this submission.");
|
|
1590
|
+
const issuedAt = this.options.now?.() ?? Math.floor(Date.now() / 1e3);
|
|
1591
|
+
return { accountId: this.options.accountId, principalId: `bot:${this.options.grantId}`, issuedAt, expiresAt: issuedAt + 300, nonce: newRequestNonce() };
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/rest/client/index.ts
|
|
1596
|
+
var RestClientError = class extends Error {
|
|
1597
|
+
constructor(code, message2, status, payFailure) {
|
|
1598
|
+
super(message2);
|
|
1599
|
+
this.code = code;
|
|
1600
|
+
this.status = status;
|
|
1601
|
+
this.name = "RestClientError";
|
|
1602
|
+
if (payFailure) this.payFailure = payFailure;
|
|
1603
|
+
}
|
|
1604
|
+
code;
|
|
1605
|
+
status;
|
|
1606
|
+
/** Why this refused payment attempt would revert, when its calls say. Advisory and for this attempt only; a changed minimum needs a fresh preparation and review. */
|
|
1607
|
+
payFailure;
|
|
1608
|
+
};
|
|
1609
|
+
function invalid5(message2) {
|
|
1610
|
+
throw new RestClientError("INVALID_INPUT", message2);
|
|
1611
|
+
}
|
|
1612
|
+
function clientAudience(value) {
|
|
1613
|
+
const audience = validateAudience(value);
|
|
1614
|
+
const url = new URL(audience);
|
|
1615
|
+
if (url.origin !== audience) return invalid5("Use the exact service origin as the audience");
|
|
1616
|
+
return audience;
|
|
1617
|
+
}
|
|
1618
|
+
function exactRequestUrl(audience, requestTarget) {
|
|
1619
|
+
if (requestTarget.length > 4096 || !/^\/(?!\/)[\x21-\x7e]*$/.test(requestTarget) || /[#\\]/.test(requestTarget) || /%(?![0-9a-fA-F]{2})/.test(requestTarget)) {
|
|
1620
|
+
return invalid5("Use an exact encoded path and query without a fragment");
|
|
1621
|
+
}
|
|
1622
|
+
const url = `${clientAudience(audience)}${requestTarget}`;
|
|
1623
|
+
const parsed = new URL(url);
|
|
1624
|
+
if (parsed.origin !== audience || `${parsed.pathname}${parsed.search}` !== requestTarget) {
|
|
1625
|
+
return invalid5("The request target would be normalized by the HTTP client");
|
|
1626
|
+
}
|
|
1627
|
+
return url;
|
|
1628
|
+
}
|
|
1629
|
+
function documentBytes(options) {
|
|
1630
|
+
if (options.json !== void 0 && options.body !== void 0) return invalid5("Supply JSON or raw bytes, not both");
|
|
1631
|
+
const method = options.method ?? "GET";
|
|
1632
|
+
if (!/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/.test(method)) return invalid5("Use an uppercase HTTP method");
|
|
1633
|
+
let body;
|
|
1634
|
+
if (options.json !== void 0) {
|
|
1635
|
+
const encoded = JSON.stringify(options.json);
|
|
1636
|
+
if (encoded === void 0) return invalid5("The JSON body cannot be encoded");
|
|
1637
|
+
body = new TextEncoder().encode(encoded);
|
|
1638
|
+
} else body = options.body?.slice() ?? new Uint8Array();
|
|
1639
|
+
if (body.byteLength > 2 * 1024 * 1024) return invalid5("Request body exceeds the client limit");
|
|
1640
|
+
if ((method === "GET" || method === "HEAD") && body.length) return invalid5("GET and HEAD requests cannot carry a body");
|
|
1641
|
+
const contentType = options.contentType ?? (options.json === void 0 ? "" : "application/json");
|
|
1642
|
+
if (contentType.length > 256 || /[\r\n\0]/.test(contentType) || new Headers({ "content-type": contentType }).get("content-type") !== contentType) {
|
|
1643
|
+
return invalid5("Invalid exact content type");
|
|
1644
|
+
}
|
|
1645
|
+
return { body, contentType, method };
|
|
1646
|
+
}
|
|
1647
|
+
async function signBytes(config, options, bytes3) {
|
|
1648
|
+
const audience = clientAudience(config.audience);
|
|
1649
|
+
const url = exactRequestUrl(audience, options.requestTarget);
|
|
1650
|
+
parseAccountId(config.accountId);
|
|
1651
|
+
if (!isAddress9(config.signer.address)) return invalid5("Invalid signer address");
|
|
1652
|
+
const issuedAt = config.now?.() ?? Math.floor(Date.now() / 1e3);
|
|
1653
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 1) return invalid5("Invalid client clock");
|
|
1654
|
+
const nonce = options.nonce ?? newRequestNonce();
|
|
1655
|
+
if (!/^0x[0-9a-f]{64}$/.test(nonce)) return invalid5("Invalid request nonce");
|
|
1656
|
+
const idempotencyKey = options.idempotencyKey ?? "";
|
|
1657
|
+
if (idempotencyKey && !/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey)) return invalid5("Invalid idempotency key");
|
|
1658
|
+
const grantId = config.grantId ?? "";
|
|
1659
|
+
if (grantId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(grantId)) return invalid5("Invalid bot grant ID");
|
|
1660
|
+
const claims2 = {
|
|
1661
|
+
accountId: config.accountId,
|
|
1662
|
+
signer: getAddress5(config.signer.address),
|
|
1663
|
+
grantId,
|
|
1664
|
+
method: bytes3.method,
|
|
1665
|
+
requestTarget: options.requestTarget,
|
|
1666
|
+
contentType: bytes3.contentType,
|
|
1667
|
+
bodyHash: keccak2569(bytes3.body),
|
|
1668
|
+
issuedAt,
|
|
1669
|
+
expiresAt: issuedAt + 300,
|
|
1670
|
+
nonce,
|
|
1671
|
+
idempotencyKey
|
|
1672
|
+
};
|
|
1673
|
+
const signature2 = await config.signer.signTypedData(buildRequestTypedData(audience, claims2));
|
|
1674
|
+
const headers = new Headers({
|
|
1675
|
+
[REST_AUTH_HEADERS.account]: claims2.accountId,
|
|
1676
|
+
[REST_AUTH_HEADERS.signer]: claims2.signer,
|
|
1677
|
+
[REST_AUTH_HEADERS.issuedAt]: String(issuedAt),
|
|
1678
|
+
[REST_AUTH_HEADERS.expiresAt]: String(claims2.expiresAt),
|
|
1679
|
+
[REST_AUTH_HEADERS.nonce]: nonce,
|
|
1680
|
+
[REST_AUTH_HEADERS.signature]: signature2,
|
|
1681
|
+
accept: "application/json"
|
|
1682
|
+
});
|
|
1683
|
+
if (bytes3.contentType) headers.set("content-type", bytes3.contentType);
|
|
1684
|
+
if (grantId) headers.set(REST_AUTH_HEADERS.grant, grantId);
|
|
1685
|
+
if (idempotencyKey) headers.set(REST_AUTH_HEADERS.idempotencyKey, idempotencyKey);
|
|
1686
|
+
return { url, method: bytes3.method, body: bytes3.body, headers, claims: claims2 };
|
|
1687
|
+
}
|
|
1688
|
+
async function prepareSignedRequest(config, options) {
|
|
1689
|
+
return signBytes(config, options, documentBytes(options));
|
|
1690
|
+
}
|
|
1691
|
+
async function boundedJson(response, maximum, signal) {
|
|
1692
|
+
const declared = response.headers.get("content-length");
|
|
1693
|
+
if (declared && Number(declared) > maximum) {
|
|
1694
|
+
void response.body?.cancel().catch(() => void 0);
|
|
1695
|
+
throw new RestClientError("RESPONSE_TOO_LARGE", "Response exceeds the client limit");
|
|
1696
|
+
}
|
|
1697
|
+
if (!response.headers.get("content-type")?.toLowerCase().includes("json")) {
|
|
1698
|
+
void response.body?.cancel().catch(() => void 0);
|
|
1699
|
+
throw new RestClientError("INVALID_RESPONSE", "Expected a JSON response", response.status);
|
|
1700
|
+
}
|
|
1701
|
+
const reader = response.body?.getReader();
|
|
1702
|
+
if (!reader) throw new RestClientError("INVALID_RESPONSE", "The response body is missing", response.status);
|
|
1703
|
+
const cancel = () => {
|
|
1704
|
+
void reader.cancel().catch(() => void 0);
|
|
1705
|
+
};
|
|
1706
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
1707
|
+
const chunks = [];
|
|
1708
|
+
let total = 0;
|
|
1709
|
+
try {
|
|
1710
|
+
signal.throwIfAborted();
|
|
1711
|
+
for (; ; ) {
|
|
1712
|
+
const { value, done } = await reader.read();
|
|
1713
|
+
signal.throwIfAborted();
|
|
1714
|
+
if (done) break;
|
|
1715
|
+
total += value.byteLength;
|
|
1716
|
+
if (total > maximum) throw new RestClientError("RESPONSE_TOO_LARGE", "Response exceeds the client limit");
|
|
1717
|
+
chunks.push(value);
|
|
1718
|
+
}
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
cancel();
|
|
1721
|
+
throw error;
|
|
1722
|
+
} finally {
|
|
1723
|
+
signal.removeEventListener("abort", cancel);
|
|
1724
|
+
reader.releaseLock();
|
|
1725
|
+
}
|
|
1726
|
+
const bytes3 = new Uint8Array(total);
|
|
1727
|
+
let offset = 0;
|
|
1728
|
+
for (const chunk of chunks) {
|
|
1729
|
+
bytes3.set(chunk, offset);
|
|
1730
|
+
offset += chunk.length;
|
|
1731
|
+
}
|
|
1732
|
+
try {
|
|
1733
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes3));
|
|
1734
|
+
} catch {
|
|
1735
|
+
throw new RestClientError("INVALID_RESPONSE", "The response is not valid JSON", response.status);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
async function readPublicRestJson(audience, requestTarget, transport = fetch) {
|
|
1739
|
+
const controller = new AbortController();
|
|
1740
|
+
const timer = setTimeout(() => controller.abort(), 15e3);
|
|
1741
|
+
try {
|
|
1742
|
+
const response = await transport(exactRequestUrl(clientAudience(audience), requestTarget), {
|
|
1743
|
+
headers: { accept: "application/json" },
|
|
1744
|
+
redirect: "error",
|
|
1745
|
+
credentials: "omit",
|
|
1746
|
+
cache: "no-store",
|
|
1747
|
+
signal: controller.signal
|
|
1748
|
+
});
|
|
1749
|
+
const result = await boundedJson(response, 2 * 1024 * 1024, controller.signal);
|
|
1750
|
+
if (!response.ok) throw new RestClientError("DISCOVERY_UNAVAILABLE", "This host has not enabled the requested wallet capability.", response.status);
|
|
1751
|
+
return result;
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
if (error instanceof RestClientError) throw error;
|
|
1754
|
+
throw new RestClientError("DISCOVERY_UNAVAILABLE", "Wallet discovery could not be loaded.");
|
|
1755
|
+
} finally {
|
|
1756
|
+
clearTimeout(timer);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
var SignedRestClient2 = class {
|
|
1760
|
+
config;
|
|
1761
|
+
constructor(config) {
|
|
1762
|
+
this.config = { ...config, audience: clientAudience(config.audience) };
|
|
1763
|
+
parseAccountId(config.accountId);
|
|
1764
|
+
}
|
|
1765
|
+
async request(options) {
|
|
1766
|
+
const retries = options.retries ?? 0;
|
|
1767
|
+
const timeoutMs = options.timeoutMs ?? this.config.timeoutMs ?? 15e3;
|
|
1768
|
+
const maxBytes = this.config.maxResponseBytes ?? 2 * 1024 * 1024;
|
|
1769
|
+
if (!Number.isSafeInteger(retries) || retries < 0 || retries > 2 || options.nonce !== void 0 && retries > 0) return invalid5("Use at most two retries; proof-bound requests cannot auto-retry");
|
|
1770
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4 || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 5 * 1024 * 1024) return invalid5("Invalid client limits");
|
|
1771
|
+
const bytes3 = documentBytes(options);
|
|
1772
|
+
if (retries && !["GET", "HEAD"].includes(bytes3.method) && !options.idempotencyKey) return invalid5("Mutation retries require an idempotency key");
|
|
1773
|
+
for (let attempt = 0; ; attempt++) {
|
|
1774
|
+
let prepared;
|
|
1775
|
+
try {
|
|
1776
|
+
prepared = await signBytes(this.config, options, bytes3);
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
const failure = error instanceof RestClientError ? new RestClientError(error.code, error.message, error.status) : new Error(error instanceof Error ? error.message : "The API request could not be signed.");
|
|
1779
|
+
if (error && typeof error === "object") {
|
|
1780
|
+
if ("name" in error && typeof error.name === "string") failure.name = error.name;
|
|
1781
|
+
if ("code" in error && ["number", "string"].includes(typeof error.code))
|
|
1782
|
+
Object.defineProperty(failure, "code", { value: error.code, enumerable: true });
|
|
1783
|
+
}
|
|
1784
|
+
Object.defineProperty(failure, "requestNotSent", { value: attempt === 0, enumerable: true });
|
|
1785
|
+
throw failure;
|
|
1786
|
+
}
|
|
1787
|
+
const controller = new AbortController();
|
|
1788
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1789
|
+
try {
|
|
1790
|
+
const response = await (this.config.fetch ?? fetch)(prepared.url, {
|
|
1791
|
+
method: prepared.method,
|
|
1792
|
+
headers: prepared.headers,
|
|
1793
|
+
...prepared.body.length ? { body: prepared.body } : {},
|
|
1794
|
+
redirect: "error",
|
|
1795
|
+
credentials: "omit",
|
|
1796
|
+
cache: "no-store",
|
|
1797
|
+
signal: controller.signal
|
|
1798
|
+
});
|
|
1799
|
+
if ([408, 429, 502, 503, 504].includes(response.status) && attempt < retries) {
|
|
1800
|
+
void response.body?.cancel().catch(() => void 0);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
const result = await boundedJson(response, maxBytes, controller.signal);
|
|
1804
|
+
if (!response.ok) {
|
|
1805
|
+
const error = result && typeof result === "object" && "error" in result ? result.error : result;
|
|
1806
|
+
const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z0-9_]{1,80}$/.test(error.code) ? error.code : "REQUEST_FAILED";
|
|
1807
|
+
throw new RestClientError(
|
|
1808
|
+
code,
|
|
1809
|
+
`The service rejected this request (${response.status}, ${code})`,
|
|
1810
|
+
response.status,
|
|
1811
|
+
error && typeof error === "object" && "payFailure" in error ? this.config.payFailure?.(error.payFailure) : void 0
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
return result;
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
if (error instanceof RestClientError) throw error;
|
|
1817
|
+
if (attempt >= retries) throw new RestClientError(controller.signal.aborted ? "TIMEOUT" : "NETWORK_ERROR", controller.signal.aborted ? "The request timed out; its outcome may be unknown" : "The request did not complete; its outcome may be unknown");
|
|
1818
|
+
} finally {
|
|
1819
|
+
clearTimeout(timer);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
async function createBotRegistration(audience, proof, signer) {
|
|
1825
|
+
if (!isCanonicalGrantScopes(proof.scopes)) return invalid5("Choose exactly read, read + plan, or read + plan + relay in that order");
|
|
1826
|
+
if (proof.botAddress.toLowerCase() !== signer.address.toLowerCase()) return invalid5("The bot signer must match the registration address");
|
|
1827
|
+
const proofSignature = await signer.signTypedData(buildBotProofTypedData(audience, proof));
|
|
1828
|
+
return parseBotRegistration({
|
|
1829
|
+
format: "juicebox-center-bot-registration-v1",
|
|
1830
|
+
audience: clientAudience(audience),
|
|
1831
|
+
accountId: proof.accountId,
|
|
1832
|
+
ownerRequestNonce: proof.ownerRequestNonce,
|
|
1833
|
+
registration: { botAddress: proof.botAddress, scopes: [...proof.scopes], expiresAt: proof.expiresAt, label: proof.label, proofSignature }
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
function parseBotRegistration(value) {
|
|
1837
|
+
const object3 = (item) => !!item && typeof item === "object" && !Array.isArray(item);
|
|
1838
|
+
if (!object3(value) || Object.keys(value).some((key) => !["format", "audience", "accountId", "ownerRequestNonce", "registration"].includes(key)) || value.format !== "juicebox-center-bot-registration-v1" || typeof value.audience !== "string" || typeof value.accountId !== "string" || typeof value.ownerRequestNonce !== "string" || !/^0x[0-9a-f]{64}$/.test(value.ownerRequestNonce) || !object3(value.registration)) return invalid5("Invalid public bot registration document");
|
|
1839
|
+
const r = value.registration;
|
|
1840
|
+
if (Object.keys(r).some((key) => !["botAddress", "scopes", "expiresAt", "label", "proofSignature"].includes(key)) || typeof r.botAddress !== "string" || !isAddress9(r.botAddress) || !isCanonicalGrantScopes(r.scopes) || !Number.isSafeInteger(r.expiresAt) || Number(r.expiresAt) < 1 || typeof r.label !== "string" || new TextEncoder().encode(r.label).length > 120 || typeof r.proofSignature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(r.proofSignature)) return invalid5("Invalid public bot proof fields");
|
|
1841
|
+
parseAccountId(value.accountId);
|
|
1842
|
+
return {
|
|
1843
|
+
format: "juicebox-center-bot-registration-v1",
|
|
1844
|
+
audience: clientAudience(value.audience),
|
|
1845
|
+
accountId: value.accountId,
|
|
1846
|
+
ownerRequestNonce: value.ownerRequestNonce,
|
|
1847
|
+
registration: { botAddress: getAddress5(r.botAddress), scopes: [...r.scopes], expiresAt: Number(r.expiresAt), label: r.label, proofSignature: r.proofSignature }
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/rest/client/walletPayments.ts
|
|
1852
|
+
import { sha256 as sha2564, stringToHex as stringToHex6 } from "viem";
|
|
1853
|
+
|
|
1854
|
+
// src/rest/client/paymentStates.ts
|
|
1855
|
+
var settledPaymentStates = ["paid", "reverted", "cancelled", "expired", "refused"];
|
|
1856
|
+
var isSettledPayment = (status) => settledPaymentStates.includes(status);
|
|
1857
|
+
|
|
1858
|
+
// src/rest/smartAccounts/passkeySignatures.ts
|
|
1859
|
+
import {
|
|
1860
|
+
concatHex as concatHex5,
|
|
1861
|
+
getAddress as getAddress6,
|
|
1862
|
+
hashDomain,
|
|
1863
|
+
hashStruct,
|
|
1864
|
+
hashTypedData as hashTypedData5,
|
|
1865
|
+
keccak256 as keccak25610,
|
|
1866
|
+
recoverAddress as recoverAddress4,
|
|
1867
|
+
size as size2,
|
|
1868
|
+
sliceHex as sliceHex3,
|
|
1869
|
+
toHex as toHex5,
|
|
1870
|
+
zeroAddress as zeroAddress2
|
|
1871
|
+
} from "viem";
|
|
1872
|
+
var MAX_PACKED_BYTES = 8192 - 20;
|
|
1873
|
+
var MAX_CONTRACT_BYTES = 4096;
|
|
1874
|
+
var MAX_OWNERS = 16;
|
|
1875
|
+
function fail2(message2) {
|
|
1876
|
+
throw new RestError(400, "SMART_ACCOUNT_ENVELOPE_INVALID", message2);
|
|
1877
|
+
}
|
|
1878
|
+
function bytes(value, label, minimum, maximum) {
|
|
1879
|
+
if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(value) || (value.length - 2) / 2 < minimum || (value.length - 2) / 2 > maximum)
|
|
1880
|
+
fail2(`${label} must contain ${minimum} to ${maximum} complete bytes.`);
|
|
1881
|
+
return value.toLowerCase();
|
|
1882
|
+
}
|
|
1883
|
+
function thresholdChecked(threshold) {
|
|
1884
|
+
if (!Number.isInteger(threshold) || threshold < 1 || threshold > MAX_OWNERS)
|
|
1885
|
+
fail2("Supply a current owner threshold between 1 and 16.");
|
|
1886
|
+
}
|
|
1887
|
+
function validityPrefix(input) {
|
|
1888
|
+
const after = integer(input.validAfter, "validAfter", false, 48);
|
|
1889
|
+
const until = integer(input.validUntil, "validUntil", false, 48);
|
|
1890
|
+
if (until <= after) fail2("Owner operations require a finite ordered validity interval.");
|
|
1891
|
+
return concatHex5([toHex5(after, { size: 6 }), toHex5(until, { size: 6 })]);
|
|
1892
|
+
}
|
|
1893
|
+
function ecdsaChecked(value) {
|
|
1894
|
+
const signature2 = bytes(value, "ECDSA signature", 65, 65);
|
|
1895
|
+
return canonicalEoaSignature(signature2);
|
|
1896
|
+
}
|
|
1897
|
+
function decodeSafeOwnerSignatures(value, threshold) {
|
|
1898
|
+
thresholdChecked(threshold);
|
|
1899
|
+
const data = bytes(value, "packed owner signatures", 65 * threshold, MAX_PACKED_BYTES);
|
|
1900
|
+
let dynamicOffset = 65 * threshold;
|
|
1901
|
+
const result = [];
|
|
1902
|
+
for (let i = 0; i < threshold; i++) {
|
|
1903
|
+
const entry = sliceHex3(data, i * 65, (i + 1) * 65);
|
|
1904
|
+
const v = Number(BigInt(sliceHex3(entry, 64, 65)));
|
|
1905
|
+
if (v !== 0) {
|
|
1906
|
+
result.push({ kind: "ecdsa", signature: ecdsaChecked(entry) });
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
const ownerWord = BigInt(sliceHex3(entry, 0, 32));
|
|
1910
|
+
if (ownerWord === 0n || ownerWord >> 160n) fail2("Contract owner address must use canonical zero padding.");
|
|
1911
|
+
const offset = BigInt(sliceHex3(entry, 32, 64));
|
|
1912
|
+
if (offset !== BigInt(dynamicOffset) || dynamicOffset + 32 > size2(data))
|
|
1913
|
+
fail2("Contract signature offsets must follow the static table without gaps, aliases or overlap.");
|
|
1914
|
+
const length = BigInt(sliceHex3(data, dynamicOffset, dynamicOffset + 32));
|
|
1915
|
+
if (length < 1n || length > BigInt(MAX_CONTRACT_BYTES) || BigInt(dynamicOffset + 32) + length > BigInt(size2(data)))
|
|
1916
|
+
fail2("Contract signature length is outside the bounded envelope.");
|
|
1917
|
+
result.push({
|
|
1918
|
+
kind: "contract",
|
|
1919
|
+
owner: getAddress6(toHex5(ownerWord, { size: 20 })),
|
|
1920
|
+
signature: sliceHex3(data, dynamicOffset + 32, dynamicOffset + 32 + Number(length))
|
|
1921
|
+
});
|
|
1922
|
+
dynamicOffset += 32 + Number(length);
|
|
1923
|
+
}
|
|
1924
|
+
if (dynamicOffset !== size2(data)) fail2("Owner signatures cannot contain trailing bytes or extra entries.");
|
|
1925
|
+
return result;
|
|
1926
|
+
}
|
|
1927
|
+
function decodeSafe7579PasskeyOwnerSignature(input) {
|
|
1928
|
+
const signature2 = bytes(input.signature, "SafeOp owner envelope", 77, 12 + MAX_PACKED_BYTES);
|
|
1929
|
+
if (sliceHex3(signature2, 0, 12) !== validityPrefix(input))
|
|
1930
|
+
fail2("Owner signature validity differs from its review.");
|
|
1931
|
+
return decodeSafeOwnerSignatures(sliceHex3(signature2, 12), input.threshold);
|
|
1932
|
+
}
|
|
1933
|
+
function safe7579PasskeyOwnerSigningPayload(input) {
|
|
1934
|
+
const payload = safe7579OwnerSigningPayload(input);
|
|
1935
|
+
const original = payload.typedData;
|
|
1936
|
+
const typedData = {
|
|
1937
|
+
...original,
|
|
1938
|
+
domain: { ...original.domain, chainId: BigInt(original.domain.chainId) },
|
|
1939
|
+
message: {
|
|
1940
|
+
...original.message,
|
|
1941
|
+
nonce: BigInt(original.message.nonce),
|
|
1942
|
+
verificationGasLimit: BigInt(original.message.verificationGasLimit),
|
|
1943
|
+
callGasLimit: BigInt(original.message.callGasLimit),
|
|
1944
|
+
preVerificationGas: BigInt(original.message.preVerificationGas),
|
|
1945
|
+
maxPriorityFeePerGas: BigInt(original.message.maxPriorityFeePerGas),
|
|
1946
|
+
maxFeePerGas: BigInt(original.message.maxFeePerGas),
|
|
1947
|
+
validAfter: Number(original.message.validAfter),
|
|
1948
|
+
validUntil: Number(original.message.validUntil)
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
const signedData = concatHex5([
|
|
1952
|
+
"0x1901",
|
|
1953
|
+
hashDomain({ domain: { ...typedData.domain, chainId: BigInt(input.chainId) }, types: typedData.types }),
|
|
1954
|
+
hashStruct({ data: typedData.message, primaryType: typedData.primaryType, types: typedData.types })
|
|
1955
|
+
]);
|
|
1956
|
+
if (keccak25610(signedData) !== payload.digest) fail2("SafeOp preimage differs from its approved digest.");
|
|
1957
|
+
return { ...payload, signedData };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/rest/userOperations/semantics.ts
|
|
1961
|
+
import { decodeEventLog, decodeFunctionData as decodeFunctionData3, encodeAbiParameters as encodeAbiParameters7, encodeEventTopics, encodeFunctionData as encodeFunctionData3, erc20Abi, getAddress as getAddress7, isAddress as isAddress10, zeroAddress as zeroAddress3 } from "viem";
|
|
1962
|
+
var terminalAbi = [{ "name": "pay", "type": "function", "stateMutability": "payable", "inputs": [{ "type": "uint256", "name": "projectId" }, { "type": "address", "name": "token" }, { "type": "uint256", "name": "amount" }, { "type": "address", "name": "beneficiary" }, { "type": "uint256", "name": "minReturnedTokens" }, { "type": "string", "name": "memo" }, { "type": "bytes", "name": "metadata" }], "outputs": [{ "type": "uint256" }] }, { "name": "Pay", "type": "event", "inputs": [{ "type": "uint256", "name": "rulesetId", "indexed": true }, { "type": "uint256", "name": "rulesetCycleNumber", "indexed": true }, { "type": "uint256", "name": "projectId", "indexed": true }, { "type": "address", "name": "payer" }, { "type": "address", "name": "beneficiary" }, { "type": "uint256", "name": "amount" }, { "type": "uint256", "name": "newlyIssuedTokenCount" }, { "type": "string", "name": "memo" }, { "type": "bytes", "name": "metadata" }, { "type": "address", "name": "caller" }] }];
|
|
1963
|
+
var same2 = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
1964
|
+
var bytes2 = (v, max = 32768) => typeof v === "string" && v.length <= 2 + max * 2 && /^0x(?:[0-9a-fA-F]{2})*$/.test(v);
|
|
1965
|
+
var address2 = (v) => {
|
|
1966
|
+
if (typeof v !== "string" || !isAddress10(v) || same2(v, zeroAddress3)) throw new Error("address");
|
|
1967
|
+
return getAddress7(v);
|
|
1968
|
+
};
|
|
1969
|
+
var object = (v) => {
|
|
1970
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || ![Object.prototype, null].includes(Object.getPrototypeOf(v))) throw new Error("object");
|
|
1971
|
+
return v;
|
|
1972
|
+
};
|
|
1973
|
+
function snapshot(input, maxBytes) {
|
|
1974
|
+
let nodes = 0, serializedBytes = 0;
|
|
1975
|
+
const count = (text) => {
|
|
1976
|
+
serializedBytes += new TextEncoder().encode(text).length;
|
|
1977
|
+
if (serializedBytes > maxBytes) throw new Error("bound");
|
|
1978
|
+
};
|
|
1979
|
+
const copy = (v, depth) => {
|
|
1980
|
+
if (++nodes > 3e4 || depth > 20) throw new Error("bound");
|
|
1981
|
+
if (v === null || typeof v === "boolean") {
|
|
1982
|
+
count(String(v));
|
|
1983
|
+
return v;
|
|
1984
|
+
}
|
|
1985
|
+
if (typeof v === "string") {
|
|
1986
|
+
if (v.length > maxBytes) throw new Error("bound");
|
|
1987
|
+
count(JSON.stringify(v));
|
|
1988
|
+
return v;
|
|
1989
|
+
}
|
|
1990
|
+
if (typeof v === "number" && Number.isSafeInteger(v)) {
|
|
1991
|
+
count(String(v));
|
|
1992
|
+
return v;
|
|
1993
|
+
}
|
|
1994
|
+
if (typeof v === "bigint" && v >= 0n && v < 2n ** 256n) {
|
|
1995
|
+
count(JSON.stringify(String(v)));
|
|
1996
|
+
return String(v);
|
|
1997
|
+
}
|
|
1998
|
+
if (!v || typeof v !== "object") throw new Error("data");
|
|
1999
|
+
const array = Array.isArray(v);
|
|
2000
|
+
if (!array) object(v);
|
|
2001
|
+
if (Object.getOwnPropertySymbols(v).length) throw new Error("symbol");
|
|
2002
|
+
const descriptors = Object.getOwnPropertyDescriptors(v);
|
|
2003
|
+
const entries = Object.entries(descriptors);
|
|
2004
|
+
if (entries.length > 3e4) throw new Error("bound");
|
|
2005
|
+
if (array) {
|
|
2006
|
+
const length = descriptors.length;
|
|
2007
|
+
if (!length || !("value" in length) || !Number.isSafeInteger(length.value) || length.value > 3e4 || length.value !== entries.length - 1) throw new Error("array bound");
|
|
2008
|
+
for (let i = 0; i < length.value; i++) if (!Object.hasOwn(descriptors, String(i))) throw new Error("sparse array");
|
|
2009
|
+
}
|
|
2010
|
+
count("[]");
|
|
2011
|
+
const out = array ? [] : /* @__PURE__ */ Object.create(null);
|
|
2012
|
+
for (const [key, desc] of entries) {
|
|
2013
|
+
if (array && key === "length") continue;
|
|
2014
|
+
if (!desc.enumerable || !("value" in desc) || array && !/^(?:0|[1-9][0-9]*)$/.test(key)) throw new Error("descriptor");
|
|
2015
|
+
count(array ? "," : `${JSON.stringify(key)}:,`);
|
|
2016
|
+
Object.defineProperty(out, key, { value: copy(desc.value, depth + 1), enumerable: true, configurable: true, writable: true });
|
|
2017
|
+
}
|
|
2018
|
+
return out;
|
|
2019
|
+
};
|
|
2020
|
+
const result = copy(input, 0);
|
|
2021
|
+
if (new TextEncoder().encode(JSON.stringify(result)).length > maxBytes) throw new Error("bound");
|
|
2022
|
+
return result;
|
|
2023
|
+
}
|
|
2024
|
+
function uint(v) {
|
|
2025
|
+
if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0) return BigInt(v);
|
|
2026
|
+
if (typeof v === "string" && v.length <= 78 && /^(?:0|[1-9][0-9]*|0x(?:0|[1-9a-fA-F][0-9a-fA-F]*))$/.test(v)) {
|
|
2027
|
+
const n = BigInt(v);
|
|
2028
|
+
if (n < 2n ** 256n) return n;
|
|
2029
|
+
}
|
|
2030
|
+
throw new Error("quantity");
|
|
2031
|
+
}
|
|
2032
|
+
function recognizeWalletV6UsdcPayment(plan, stepIndexes, config) {
|
|
2033
|
+
try {
|
|
2034
|
+
const { draft } = snapshot(plan, 262144);
|
|
2035
|
+
const indexes = snapshot(stepIndexes, 128);
|
|
2036
|
+
const trusted = snapshot(config, 1024);
|
|
2037
|
+
if (trusted.chainId !== 8453 || draft.operation !== "pay") return null;
|
|
2038
|
+
const token = address2(trusted.token), terminal2 = address2(trusted.directV6Terminal), account = address2(draft.account);
|
|
2039
|
+
if (same2(token, terminal2) || same2(account, terminal2) || same2(account, token)) return null;
|
|
2040
|
+
const calls = draft.calls;
|
|
2041
|
+
if (!Array.isArray(calls) || calls.length < 1 || calls.length > 3 || !Array.isArray(indexes) || indexes.length !== calls.length || indexes.some((v, i) => v !== i)) return null;
|
|
2042
|
+
const project = object(draft.project);
|
|
2043
|
+
if (project.chainId !== 8453 || project.version !== void 0 && project.version !== 6 || typeof project.projectId !== "string" || !/^[1-9][0-9]*$/.test(project.projectId)) return null;
|
|
2044
|
+
const projectId = uint(project.projectId);
|
|
2045
|
+
for (const [i, call] of calls.entries()) {
|
|
2046
|
+
if (call.chainId !== 8453 || call.value !== "0" || !bytes2(call.data) || !Array.isArray(call.dependsOn) || call.dependsOn.length !== (i === 0 ? 0 : 1) || i > 0 && call.dependsOn[0] !== i - 1) return null;
|
|
2047
|
+
}
|
|
2048
|
+
const last = calls[calls.length - 1];
|
|
2049
|
+
if (!same2(address2(last.to), terminal2)) return null;
|
|
2050
|
+
const decoded = decodeFunctionData3({ abi: terminalAbi, data: last.data });
|
|
2051
|
+
if (decoded.functionName !== "pay" || !same2(encodeFunctionData3({ abi: terminalAbi, functionName: "pay", args: decoded.args }), last.data)) return null;
|
|
2052
|
+
const [id3, paidToken, amount2, to, minimum, memo, metadata] = decoded.args;
|
|
2053
|
+
if (id3 !== projectId || !same2(paidToken, token) || amount2 <= 0n || amount2 === 2n ** 256n - 1n || !bytes2(metadata, 16384) || new TextEncoder().encode(memo).length > 256) return null;
|
|
2054
|
+
const beneficiary = address2(to);
|
|
2055
|
+
for (let i = 0; i < calls.length - 1; i++) {
|
|
2056
|
+
const call = calls[i];
|
|
2057
|
+
if (!same2(address2(call.to), token)) return null;
|
|
2058
|
+
const approval = decodeFunctionData3({ abi: erc20Abi, data: call.data });
|
|
2059
|
+
if (approval.functionName !== "approve" || !same2(approval.args[0], terminal2) || approval.args[1] !== (calls.length === 3 && i === 0 ? 0n : amount2) || !same2(encodeFunctionData3({ abi: erc20Abi, functionName: "approve", args: approval.args }), call.data)) return null;
|
|
2060
|
+
}
|
|
2061
|
+
const summary = object(draft.summary), payment = object(summary.payment), quotedProject = object(summary.project);
|
|
2062
|
+
if (summary.operation !== "pay" || !same2(address2(summary.account), account) || summary.route !== "multi-terminal" || !same2(address2(summary.terminal), terminal2) || summary.routerGateway !== null || !Array.isArray(summary.terminalPath) || summary.terminalPath.length !== 1 || !same2(address2(summary.terminalPath[0]), terminal2) || quotedProject.chainId !== 8453 || quotedProject.projectId !== project.projectId || quotedProject.version !== void 0 && quotedProject.version !== 6 || !same2(address2(payment.token), token) || payment.amount !== String(amount2) || payment.unit !== "token-base-units" || !same2(address2(summary.beneficiary), beneficiary) || summary.minimumBeneficiaryTokenCount !== String(minimum) || typeof summary.metadata !== "string" || !same2(summary.metadata, metadata)) return null;
|
|
2063
|
+
return Object.freeze({
|
|
2064
|
+
kind: "v6-usdc-pay",
|
|
2065
|
+
chainId: 8453,
|
|
2066
|
+
account,
|
|
2067
|
+
token,
|
|
2068
|
+
terminal: terminal2,
|
|
2069
|
+
projectId: String(id3),
|
|
2070
|
+
amount: String(amount2),
|
|
2071
|
+
beneficiary,
|
|
2072
|
+
minimumReturnedTokens: String(minimum),
|
|
2073
|
+
memo,
|
|
2074
|
+
metadata,
|
|
2075
|
+
stepIndexes: Object.freeze([...indexes]),
|
|
2076
|
+
approvalStepIndexes: Object.freeze(indexes.slice(0, -1)),
|
|
2077
|
+
paymentStepIndex: calls.length - 1,
|
|
2078
|
+
resetAllowance: calls.length === 3
|
|
2079
|
+
});
|
|
2080
|
+
} catch {
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/rest/wallet/sharedHandoff.ts
|
|
2086
|
+
import { hashTypedData as hashTypedData6, hexToBytes, keccak256 as keccak25611, sha256 as sha2563, stringToHex as stringToHex5, toHex as toHex6 } from "viem";
|
|
2087
|
+
var requestMessageTypes = [
|
|
2088
|
+
{ name: "version", type: "string" },
|
|
2089
|
+
{ name: "issuer", type: "string" },
|
|
2090
|
+
{ name: "origin", type: "string" },
|
|
2091
|
+
{ name: "callbackUri", type: "string" },
|
|
2092
|
+
{ name: "audience", type: "string" },
|
|
2093
|
+
{ name: "appGeneration", type: "uint64" },
|
|
2094
|
+
{ name: "requestKey", type: "address" },
|
|
2095
|
+
{ name: "state", type: "bytes32" },
|
|
2096
|
+
{ name: "codeChallenge", type: "bytes32" },
|
|
2097
|
+
{ name: "nonce", type: "bytes32" },
|
|
2098
|
+
{ name: "issuedAtMs", type: "uint64" },
|
|
2099
|
+
{ name: "expiresAtMs", type: "uint64" }
|
|
2100
|
+
];
|
|
2101
|
+
var exchangeMessageTypes = [
|
|
2102
|
+
...requestMessageTypes,
|
|
2103
|
+
{ name: "intentId", type: "bytes32" },
|
|
2104
|
+
{ name: "codeHash", type: "bytes32" }
|
|
2105
|
+
];
|
|
2106
|
+
for (const field of exchangeMessageTypes) Object.freeze(field);
|
|
2107
|
+
var requestTypes2 = Object.freeze({ WalletHandoffRequest: Object.freeze(requestMessageTypes) });
|
|
2108
|
+
var exchangeTypes = Object.freeze({ WalletHandoffExchange: Object.freeze(exchangeMessageTypes) });
|
|
2109
|
+
var launchTypes = Object.freeze({ WalletHandoffLaunch: Object.freeze([
|
|
2110
|
+
Object.freeze({ name: "intentId", type: "bytes32" }),
|
|
2111
|
+
Object.freeze({ name: "requestDigest", type: "bytes32" })
|
|
2112
|
+
]) });
|
|
2113
|
+
function base64url(bytes3) {
|
|
2114
|
+
return btoa(String.fromCharCode(...bytes3)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
2115
|
+
}
|
|
2116
|
+
function validateWalletHandoffToken(value) {
|
|
2117
|
+
if (typeof value !== "string" || value.length !== 43 || !/^[A-Za-z0-9_-]{43}$/.test(value)) throw new Error("Invalid wallet handoff token.");
|
|
2118
|
+
const bytes3 = Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/") + "="), (character) => character.charCodeAt(0));
|
|
2119
|
+
if (bytes3.length !== 32 || base64url(bytes3) !== value) throw new Error("Invalid wallet handoff token.");
|
|
2120
|
+
return value;
|
|
2121
|
+
}
|
|
2122
|
+
function tokenHex(value) {
|
|
2123
|
+
return toHex6(Uint8Array.from(atob(validateWalletHandoffToken(value).replaceAll("-", "+").replaceAll("_", "/") + "="), (character) => character.charCodeAt(0)));
|
|
2124
|
+
}
|
|
2125
|
+
function walletHandoffPkceChallenge(value) {
|
|
2126
|
+
if (typeof value !== "string" || value.length < 43 || value.length > 128 || !/^[A-Za-z0-9._~-]+$/.test(value)) throw new Error("Invalid wallet handoff verifier.");
|
|
2127
|
+
return base64url(hexToBytes(sha2563(stringToHex5(value))));
|
|
2128
|
+
}
|
|
2129
|
+
function walletHandoffCodeHash(value) {
|
|
2130
|
+
return sha2563(stringToHex5("center-wallet-handoff-code-v1\0" + validateWalletHandoffToken(value)));
|
|
2131
|
+
}
|
|
2132
|
+
function domain2(request) {
|
|
2133
|
+
return { name: "Juicebox Center Wallet Handoff", version: "1", chainId: 8453, salt: keccak25611(stringToHex5(request.issuer)) };
|
|
2134
|
+
}
|
|
2135
|
+
function message(request) {
|
|
2136
|
+
return {
|
|
2137
|
+
...request,
|
|
2138
|
+
state: tokenHex(request.state),
|
|
2139
|
+
codeChallenge: tokenHex(request.codeChallenge),
|
|
2140
|
+
appGeneration: BigInt(request.appGeneration),
|
|
2141
|
+
issuedAtMs: BigInt(request.issuedAtMs),
|
|
2142
|
+
expiresAtMs: BigInt(request.expiresAtMs)
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
function walletHandoffRequestDocument(request) {
|
|
2146
|
+
return { domain: domain2(request), types: requestTypes2, primaryType: "WalletHandoffRequest", message: message(request) };
|
|
2147
|
+
}
|
|
2148
|
+
function walletHandoffExchangeDocument(input) {
|
|
2149
|
+
return {
|
|
2150
|
+
domain: domain2(input.request),
|
|
2151
|
+
types: exchangeTypes,
|
|
2152
|
+
primaryType: "WalletHandoffExchange",
|
|
2153
|
+
message: { ...message(input.request), intentId: tokenHex(input.intentId), codeHash: input.codeHash }
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
function walletHandoffLaunchDocument(input) {
|
|
2157
|
+
return {
|
|
2158
|
+
domain: domain2(input.request),
|
|
2159
|
+
types: launchTypes,
|
|
2160
|
+
primaryType: "WalletHandoffLaunch",
|
|
2161
|
+
message: { intentId: tokenHex(input.intentId), requestDigest: hashTypedData6(walletHandoffRequestDocument(input.request)) }
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// src/rest/client/walletPayments.ts
|
|
2166
|
+
var maximumBytes = 1048576;
|
|
2167
|
+
var reviewPath = "/api/v1/wallet/payment-reviews";
|
|
2168
|
+
var reviewIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
2169
|
+
var paymentFields = ["kind", "chainId", "account", "token", "terminal", "projectId", "amount", "beneficiary", "minimumReturnedTokens", "memo", "metadata"];
|
|
2170
|
+
var states = ["reviewing", "approved", "submitting", "pending", "confirming", "paid", "reverted", "cancelled", "unknown", "expired", "refused"];
|
|
2171
|
+
var terminal = isSettledPayment;
|
|
2172
|
+
var hex32 = (value) => typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value);
|
|
2173
|
+
var uuid2 = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
2174
|
+
var integer2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2175
|
+
var same3 = (a, b) => typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
|
|
2176
|
+
var equal = (a, b) => uoCanonical(a) === uoCanonical(b);
|
|
2177
|
+
function fail3(code, message2) {
|
|
2178
|
+
throw new RestClientError(code, message2);
|
|
2179
|
+
}
|
|
2180
|
+
function mismatch() {
|
|
2181
|
+
return fail3("WALLET_PAYMENT_MISMATCH", "The payment differs from this app\u2019s exact reviewed operation.");
|
|
2182
|
+
}
|
|
2183
|
+
function snapshot2(value) {
|
|
2184
|
+
const encoded = JSON.stringify(value);
|
|
2185
|
+
if (encoded === void 0 || new TextEncoder().encode(encoded).length > maximumBytes) mismatch();
|
|
2186
|
+
return JSON.parse(encoded);
|
|
2187
|
+
}
|
|
2188
|
+
function fields(value, required2, optional2 = []) {
|
|
2189
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) mismatch();
|
|
2190
|
+
const keys = Object.keys(value);
|
|
2191
|
+
if (required2.some((key) => !keys.includes(key)) || keys.some((key) => !required2.includes(key) && !optional2.includes(key))) mismatch();
|
|
2192
|
+
return value;
|
|
2193
|
+
}
|
|
2194
|
+
function comparablePayment(payment) {
|
|
2195
|
+
return Object.fromEntries(Object.entries(payment).map(([key, value]) => [
|
|
2196
|
+
key,
|
|
2197
|
+
["account", "token", "terminal", "beneficiary", "metadata"].includes(key) && typeof value === "string" ? value.toLowerCase() : value
|
|
2198
|
+
]));
|
|
2199
|
+
}
|
|
2200
|
+
function createCenterWalletPaymentClient(options) {
|
|
2201
|
+
const { issuer, audience, callbackUri, storage, location, now } = options;
|
|
2202
|
+
const key = "center.wallet.payment.v1:" + issuer + ":" + callbackUri, historyKey = key + ":history";
|
|
2203
|
+
function raw(name = key) {
|
|
2204
|
+
try {
|
|
2205
|
+
return storage.getItem(name);
|
|
2206
|
+
} catch {
|
|
2207
|
+
return fail3("WALLET_PAYMENT_STORAGE_UNAVAILABLE", "This tab could not read its payment record.");
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
function save(value, expected) {
|
|
2211
|
+
const copy = snapshot2(value), integrity = sha2564(stringToHex6(uoCanonical(copy))), encoded = JSON.stringify({ ...copy, integrity });
|
|
2212
|
+
if (raw() !== expected) fail3("WALLET_PAYMENT_CHANGED", "This tab changed its pending payment. Resume the current payment.");
|
|
2213
|
+
try {
|
|
2214
|
+
if (new TextEncoder().encode(encoded).length > maximumBytes) throw new Error();
|
|
2215
|
+
storage.setItem(key, encoded);
|
|
2216
|
+
if (storage.getItem(key) !== encoded) throw new Error();
|
|
2217
|
+
} catch {
|
|
2218
|
+
return fail3("WALLET_PAYMENT_STORAGE_UNAVAILABLE", "This tab could not preserve its payment before continuing.");
|
|
2219
|
+
}
|
|
2220
|
+
return { value: copy, encoded };
|
|
2221
|
+
}
|
|
2222
|
+
function checkInput(input, grant, at) {
|
|
2223
|
+
try {
|
|
2224
|
+
const operation = input.operation, expected = input.expectedPayment;
|
|
2225
|
+
assertReviewedOperation(operation, input.plan, at);
|
|
2226
|
+
if (operation.chainId !== 8453 || grant.accountId !== "eip155:8453:" + operation.operation.sender.toLowerCase() || operation.session || operation.submission || operation.observation || input.plan.expiresAt <= at || !hex32(operation.commitment) || !integer2(at) || !("ownerProfile" in operation.signing) || operation.signing.ownerProfile !== "center-passkey-v1") mismatch();
|
|
2227
|
+
const signing = operation.signing;
|
|
2228
|
+
const derived = safe7579PasskeyOwnerSigningPayload({
|
|
2229
|
+
operation: operation.operation,
|
|
2230
|
+
chainId: 8453,
|
|
2231
|
+
entryPoint: operation.entryPoint,
|
|
2232
|
+
safe7579: signing.typedData.domain.verifyingContract,
|
|
2233
|
+
validAfter: signing.validAfter,
|
|
2234
|
+
validUntil: signing.validUntil
|
|
2235
|
+
});
|
|
2236
|
+
if (!equal(signing, { ...derived, ownerProfile: "center-passkey-v1" })) mismatch();
|
|
2237
|
+
const payment = recognizeWalletV6UsdcPayment(
|
|
2238
|
+
input.plan,
|
|
2239
|
+
operation.stepIndexes,
|
|
2240
|
+
{ chainId: 8453, token: expected.token, directV6Terminal: expected.terminal }
|
|
2241
|
+
);
|
|
2242
|
+
if (!payment || paymentFields.some((field) => {
|
|
2243
|
+
const actual = payment[field], wanted = expected[field];
|
|
2244
|
+
return ["account", "token", "terminal", "beneficiary", "metadata"].includes(field) ? !same3(actual, wanted) : actual !== wanted;
|
|
2245
|
+
}) || typeof expected.maximumNetworkFee !== "string" || !/^[1-9][0-9]{0,77}$/.test(expected.maximumNetworkFee) || BigInt(expected.maximumNetworkFee) >= 2n ** 256n || userOperationMaximumCost(operation.operation) > BigInt(expected.maximumNetworkFee)) mismatch();
|
|
2246
|
+
return payment;
|
|
2247
|
+
} catch {
|
|
2248
|
+
return mismatch();
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
function live(value) {
|
|
2252
|
+
let current;
|
|
2253
|
+
try {
|
|
2254
|
+
current = options.connection();
|
|
2255
|
+
} catch {
|
|
2256
|
+
current = null;
|
|
2257
|
+
}
|
|
2258
|
+
if (!current || current.grant.revokedAt !== null || current.grant.expiresAt <= Math.floor(now() / 1e3) || current.grant.audience !== audience || current.grant.callbackUri !== callbackUri || value && !equal(current.grant, value.grant))
|
|
2259
|
+
fail3("WALLET_PAYMENT_CONNECTION_CHANGED", "Reconnect the original app grant to continue this pending payment.");
|
|
2260
|
+
return current;
|
|
2261
|
+
}
|
|
2262
|
+
function checkApproval(value, saved) {
|
|
2263
|
+
const approval = fields(value, ["signature", "signedCommitment"]);
|
|
2264
|
+
if (typeof approval.signature !== "string" || !/^0x(?:[0-9a-fA-F]{2}){77,8192}$/.test(approval.signature) || !hex32(approval.signedCommitment)) mismatch();
|
|
2265
|
+
const signing = saved.operation.signing;
|
|
2266
|
+
if (!("ownerProfile" in signing)) mismatch();
|
|
2267
|
+
const entries = decodeSafe7579PasskeyOwnerSignature({
|
|
2268
|
+
signature: approval.signature,
|
|
2269
|
+
validAfter: signing.validAfter,
|
|
2270
|
+
validUntil: signing.validUntil,
|
|
2271
|
+
threshold: 1
|
|
2272
|
+
});
|
|
2273
|
+
if (entries.length !== 1 || entries[0]?.kind !== "contract" || !same3(
|
|
2274
|
+
approval.signedCommitment,
|
|
2275
|
+
userOperationCommitment({ ...saved.operation.operation, signature: approval.signature }, saved.operation.entryPoint, 8453)
|
|
2276
|
+
)) mismatch();
|
|
2277
|
+
return { signature: approval.signature, signedCommitment: approval.signedCommitment };
|
|
2278
|
+
}
|
|
2279
|
+
function checkView(input, saved, appView) {
|
|
2280
|
+
try {
|
|
2281
|
+
const value = snapshot2(input);
|
|
2282
|
+
const v = fields(value, [
|
|
2283
|
+
"version",
|
|
2284
|
+
"id",
|
|
2285
|
+
"state",
|
|
2286
|
+
"issuer",
|
|
2287
|
+
"accountId",
|
|
2288
|
+
"app",
|
|
2289
|
+
"planId",
|
|
2290
|
+
"planCommitment",
|
|
2291
|
+
"operationId",
|
|
2292
|
+
"operationCommitment",
|
|
2293
|
+
"operationHash",
|
|
2294
|
+
"stepIndexes",
|
|
2295
|
+
"operation",
|
|
2296
|
+
"chainId",
|
|
2297
|
+
"entryPoint",
|
|
2298
|
+
"safe7579",
|
|
2299
|
+
"payment",
|
|
2300
|
+
"signing",
|
|
2301
|
+
"createdAtMs",
|
|
2302
|
+
"expiresAtMs",
|
|
2303
|
+
"status",
|
|
2304
|
+
"approvedAtMs",
|
|
2305
|
+
"cancelledAtMs",
|
|
2306
|
+
"operationState",
|
|
2307
|
+
...appView ? ["approval"] : []
|
|
2308
|
+
]);
|
|
2309
|
+
const app = fields(v.app, ["origin", "callbackUri", "grantId", "grantIncarnation"]);
|
|
2310
|
+
const signing = saved.operation.signing;
|
|
2311
|
+
if (!("ownerProfile" in signing)) mismatch();
|
|
2312
|
+
const expectedSigning = { digest: signing.digest, signedData: signing.signedData, validAfter: signing.validAfter, validUntil: signing.validUntil };
|
|
2313
|
+
const payment = checkInput(saved, saved.grant, saved.startedAtMs);
|
|
2314
|
+
if (v.version !== "center-wallet-payment-review-v1" || !uuid2(v.id) || saved.review && v.id !== saved.review.id || v.state !== saved.state || v.issuer !== issuer || v.accountId !== saved.grant.accountId || app.origin !== saved.grant.origin || app.callbackUri !== callbackUri || app.grantId !== saved.grant.id || app.grantIncarnation !== saved.grant.incarnation || v.planId !== saved.plan.id || !same3(v.planCommitment, saved.plan.commitment) || v.operationId !== saved.operation.id || !same3(v.operationCommitment, saved.operation.commitment) || !same3(v.operationHash, saved.operation.operationHash) || !equal(v.stepIndexes, saved.operation.stepIndexes) || !equal(normalizeUserOperation(v.operation), normalizeUserOperation(saved.operation.operation)) || v.chainId !== 8453 || !same3(v.entryPoint, saved.operation.entryPoint) || !same3(v.safe7579, signing.typedData.domain.verifyingContract) || !equal(v.signing, expectedSigning) || !equal(comparablePayment(v.payment), comparablePayment(payment)) || !integer2(v.createdAtMs) || v.createdAtMs < saved.startedAtMs - 3e4 || v.createdAtMs > now() + 3e4 || !integer2(v.expiresAtMs) || v.expiresAtMs <= v.createdAtMs || v.expiresAtMs > saved.operation.expiresAt || v.expiresAtMs > saved.grant.expiresAt * 1e3 || v.expiresAtMs > v.createdAtMs + 3e5 || saved.review && (v.createdAtMs !== saved.review.createdAtMs || v.expiresAtMs !== saved.review.expiresAtMs) || !["pending", "approved", "cancelled"].includes(String(v.status)) || !["prepared", "submitting", "submission_unknown", "pending", "unknown", "confirming", "confirmed", "reverted", "expired", "retired"].includes(String(v.operationState))) mismatch();
|
|
2315
|
+
if (v.status === "approved" ? !integer2(v.approvedAtMs) || v.cancelledAtMs !== null : v.approvedAtMs !== null) mismatch();
|
|
2316
|
+
if (v.status === "cancelled" ? !integer2(v.cancelledAtMs) : v.cancelledAtMs !== null) mismatch();
|
|
2317
|
+
for (const at of [v.approvedAtMs, v.cancelledAtMs])
|
|
2318
|
+
if (at !== null && (!integer2(at) || at < v.createdAtMs || at > v.expiresAtMs || at > now() + 3e4)) mismatch();
|
|
2319
|
+
if (appView) {
|
|
2320
|
+
if (v.status === "approved") checkApproval(v.approval, saved);
|
|
2321
|
+
else if (v.approval !== null) mismatch();
|
|
2322
|
+
}
|
|
2323
|
+
if (saved.approval && (v.status !== "approved" || appView && !equal(v.approval, saved.approval))) mismatch();
|
|
2324
|
+
return value;
|
|
2325
|
+
} catch {
|
|
2326
|
+
return mismatch();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
function read() {
|
|
2330
|
+
const encoded = raw();
|
|
2331
|
+
if (encoded === null) return null;
|
|
2332
|
+
try {
|
|
2333
|
+
if (new TextEncoder().encode(encoded).length > maximumBytes) throw new Error();
|
|
2334
|
+
const decoded = JSON.parse(encoded), { integrity, ...value } = decoded;
|
|
2335
|
+
if (integrity !== sha2564(stringToHex6(uoCanonical(value))) || value.version !== "center-wallet-payment-client-v1" || value.issuer !== issuer || value.audience !== audience || value.callbackUri !== callbackUri || !states.includes(value.status) || typeof value.submissionStarted !== "boolean" || !/^payment-review-[0-9a-f]{64}$/.test(value.reviewKey) || !/^payment-submit-[0-9a-f]{64}$/.test(value.submissionKey) || value.reviewId !== void 0 && !reviewIdPattern.test(value.reviewId)) throw new Error();
|
|
2336
|
+
validateWalletHandoffToken(value.state);
|
|
2337
|
+
checkInput(value, value.grant, value.startedAtMs);
|
|
2338
|
+
if (value.review) checkView(value.review, value, false);
|
|
2339
|
+
if (value.approval) checkApproval(value.approval, value);
|
|
2340
|
+
if (value.submissionStarted && !value.approval) throw new Error();
|
|
2341
|
+
return { value, encoded };
|
|
2342
|
+
} catch {
|
|
2343
|
+
return fail3("WALLET_PAYMENT_STORAGE_INVALID", "The saved payment record is invalid. Preserve it for recovery.");
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
function pending() {
|
|
2347
|
+
const value = read();
|
|
2348
|
+
if (!value) fail3("WALLET_PAYMENT_MISSING", "There is no pending payment in this tab.");
|
|
2349
|
+
return value;
|
|
2350
|
+
}
|
|
2351
|
+
function status(value) {
|
|
2352
|
+
const served = value.status === "reverted" && value.observation?.state === "reverted" ? value.observation.payFailure : void 0;
|
|
2353
|
+
const payFailure = payFailureOf(served);
|
|
2354
|
+
const { payFailure: _served, ...observed } = value.observation ?? {};
|
|
2355
|
+
const observation = value.observation && { ...observed, ...payFailure ? { payFailure: served } : {} };
|
|
2356
|
+
const copy = snapshot2({
|
|
2357
|
+
status: value.status,
|
|
2358
|
+
accountId: value.grant.accountId,
|
|
2359
|
+
chainId: 8453,
|
|
2360
|
+
operationId: value.operation.id,
|
|
2361
|
+
operationHash: value.operation.operationHash,
|
|
2362
|
+
reviewId: value.review?.id ?? null,
|
|
2363
|
+
approvalUrl: value.review ? issuer + "/wallet/payment?review=" + value.review.id : null,
|
|
2364
|
+
expiresAtMs: value.review?.expiresAtMs ?? value.operation.expiresAt,
|
|
2365
|
+
expectedPayment: value.expectedPayment,
|
|
2366
|
+
...observation ? {
|
|
2367
|
+
observation,
|
|
2368
|
+
...hex32(observation.transactionHash) ? { transactionHash: observation.transactionHash } : {}
|
|
2369
|
+
} : {}
|
|
2370
|
+
});
|
|
2371
|
+
return payFailure ? { ...copy, payFailure } : copy;
|
|
2372
|
+
}
|
|
2373
|
+
function receiveView(input, saved, appView) {
|
|
2374
|
+
const reviewed = checkView(input, saved.value, appView);
|
|
2375
|
+
const { approval, ...review } = reviewed;
|
|
2376
|
+
const state = saved.value.submissionStarted ? saved.value.status : reviewed.status === "approved" ? "approved" : reviewed.status === "cancelled" ? "cancelled" : "reviewing";
|
|
2377
|
+
return save({ ...saved.value, review, status: state, ...approval ? { approval } : {} }, saved.encoded);
|
|
2378
|
+
}
|
|
2379
|
+
async function requestReview(saved) {
|
|
2380
|
+
const client = live(saved.value).client;
|
|
2381
|
+
const value = await client.request({
|
|
2382
|
+
method: "POST",
|
|
2383
|
+
requestTarget: reviewPath,
|
|
2384
|
+
json: { operationId: saved.value.operation.id, state: saved.value.state, ...saved.value.reviewId ? { id: saved.value.reviewId } : {} },
|
|
2385
|
+
idempotencyKey: saved.value.reviewKey
|
|
2386
|
+
});
|
|
2387
|
+
if (saved.value.reviewId && value?.id !== saved.value.reviewId) mismatch();
|
|
2388
|
+
return receiveView(value, saved, false);
|
|
2389
|
+
}
|
|
2390
|
+
function reviewUrl(reviewId) {
|
|
2391
|
+
if (!reviewIdPattern.test(reviewId)) fail3("WALLET_PAYMENT_INPUT_INVALID", "Choose a version 4 UUID for the review.");
|
|
2392
|
+
return issuer + "/wallet/payment?review=" + reviewId;
|
|
2393
|
+
}
|
|
2394
|
+
async function preparePayment(input, options2 = {}) {
|
|
2395
|
+
if (options2.reviewId !== void 0 && !reviewIdPattern.test(options2.reviewId)) fail3("WALLET_PAYMENT_INPUT_INVALID", "Choose a version 4 UUID for the review.");
|
|
2396
|
+
const existing = read();
|
|
2397
|
+
if (existing) {
|
|
2398
|
+
checkInput(snapshot2(input), existing.value.grant, existing.value.startedAtMs);
|
|
2399
|
+
if (input.expectedPayment.maximumNetworkFee !== existing.value.expectedPayment.maximumNetworkFee) mismatch();
|
|
2400
|
+
if (!equal(existing.value.plan, snapshot2(input.plan)) || !equal(existing.value.operation, snapshot2(input.operation)))
|
|
2401
|
+
fail3("WALLET_PAYMENT_PENDING", "Resolve this tab\u2019s existing payment before preparing another operation.");
|
|
2402
|
+
live(existing.value);
|
|
2403
|
+
if (existing.value.review) return status(existing.value);
|
|
2404
|
+
return status((await requestReview(existing)).value);
|
|
2405
|
+
}
|
|
2406
|
+
const current = live(), copy = snapshot2(input), startedAtMs = now();
|
|
2407
|
+
const payment = checkInput(copy, current.grant, startedAtMs);
|
|
2408
|
+
const expectedPayment = { ...Object.fromEntries(paymentFields.map((field) => [field, payment[field]])), maximumNetworkFee: copy.expectedPayment.maximumNetworkFee };
|
|
2409
|
+
const bytes3 = crypto.getRandomValues(new Uint8Array(32));
|
|
2410
|
+
const state = btoa(String.fromCharCode(...bytes3)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
2411
|
+
const value = {
|
|
2412
|
+
...copy,
|
|
2413
|
+
expectedPayment,
|
|
2414
|
+
version: "center-wallet-payment-client-v1",
|
|
2415
|
+
issuer,
|
|
2416
|
+
audience,
|
|
2417
|
+
callbackUri,
|
|
2418
|
+
grant: snapshot2(current.grant),
|
|
2419
|
+
startedAtMs,
|
|
2420
|
+
state,
|
|
2421
|
+
reviewKey: "payment-review-" + newRequestNonce().slice(2),
|
|
2422
|
+
...options2.reviewId ? { reviewId: options2.reviewId } : {},
|
|
2423
|
+
submissionKey: "payment-submit-" + newRequestNonce().slice(2),
|
|
2424
|
+
submissionStarted: false,
|
|
2425
|
+
status: "reviewing"
|
|
2426
|
+
};
|
|
2427
|
+
return status((await requestReview(save(value, null))).value);
|
|
2428
|
+
}
|
|
2429
|
+
async function completePayment(callbackUrl) {
|
|
2430
|
+
const incoming = callbackUrl ?? location.href();
|
|
2431
|
+
try {
|
|
2432
|
+
location.replace(callbackUri);
|
|
2433
|
+
} catch {
|
|
2434
|
+
return fail3("WALLET_PAYMENT_CALLBACK_INVALID", "The payment callback could not be cleared.");
|
|
2435
|
+
}
|
|
2436
|
+
const saved = pending();
|
|
2437
|
+
try {
|
|
2438
|
+
const url = new URL(incoming);
|
|
2439
|
+
if (incoming.length > 4096 || incoming.slice(0, incoming.indexOf("?")) !== callbackUri || url.origin + url.pathname !== callbackUri || url.username || url.password || url.hash || [...url.searchParams].length !== 3 || ["review", "state", "iss"].some((name) => url.searchParams.getAll(name).length !== 1) || url.searchParams.get("review") !== saved.value.review?.id || url.searchParams.get("state") !== saved.value.state || url.searchParams.get("iss") !== issuer) throw new Error();
|
|
2440
|
+
} catch {
|
|
2441
|
+
return fail3("WALLET_PAYMENT_CALLBACK_INVALID", "The callback does not match the original payment and app grant.");
|
|
2442
|
+
}
|
|
2443
|
+
const result = await live(saved.value).client.request({ requestTarget: reviewPath + "/" + saved.value.review.id });
|
|
2444
|
+
return status(receiveView(result, saved, true).value);
|
|
2445
|
+
}
|
|
2446
|
+
function receiveOperation(input, saved) {
|
|
2447
|
+
try {
|
|
2448
|
+
const operation = snapshot2(input), original = saved.value.operation;
|
|
2449
|
+
for (const name of ["id", "planId", "planCommitment", "commitment", "operationHash", "chainId", "entryPoint", "accountBindingId", "accountStateHash", "createdAt", "expiresAt"])
|
|
2450
|
+
if (operation[name] !== original[name]) mismatch();
|
|
2451
|
+
if (!equal(normalizeUserOperation(operation.operation), normalizeUserOperation(original.operation)) || !equal(operation.stepIndexes, original.stepIndexes) || operation.submission && !same3(operation.submission.commitment, saved.value.approval?.signedCommitment)) mismatch();
|
|
2452
|
+
if (Number.isInteger(operation.revision) && operation.revision >= 0) revisions.set(operation.id, operation.revision);
|
|
2453
|
+
const observation = operation.observation;
|
|
2454
|
+
if (observation && (!same3(observation.operationHash, original.operationHash) || observation.transactionHash !== void 0 && !hex32(observation.transactionHash))) mismatch();
|
|
2455
|
+
let result = operation.state === "pending" ? "pending" : operation.state === "confirming" ? "confirming" : "unknown";
|
|
2456
|
+
const receipt = observation?.receipt;
|
|
2457
|
+
const canonical = !!operation.submission && receipt?.canonical === true && hex32(receipt.blockHash) && hex32(receipt.transactionHash) && same3(receipt.transactionHash, observation?.transactionHash) && Number.isSafeInteger(receipt.confirmations) && receipt.confirmations > 0 && typeof receipt.blockNumber === "string" && /^(0|[1-9][0-9]{0,77})$/.test(receipt.blockNumber) && integer2(receipt.observedAt);
|
|
2458
|
+
if (operation.state === "confirmed" && observation?.state === "confirmed" && canonical && receipt?.status === "success" && observation.semantic?.status === "verified") result = "paid";
|
|
2459
|
+
if (operation.state === "reverted" && observation?.state === "reverted" && canonical && (receipt?.status === "reverted" || observation.semantic?.status === "failed")) result = "reverted";
|
|
2460
|
+
if (operation.state === "expired" && observation?.state === "expired" && observation.transactionHash === void 0 && !receipt) result = "expired";
|
|
2461
|
+
if (operation.state === "retired" && !operation.submission && !observation) result = "refused";
|
|
2462
|
+
return save({ ...saved.value, status: result, ...observation ? { observation } : {} }, saved.encoded);
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
if (error instanceof RestClientError && ["WALLET_PAYMENT_STORAGE_UNAVAILABLE", "WALLET_PAYMENT_CHANGED"].includes(error.code)) throw error;
|
|
2465
|
+
return mismatch();
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
async function submitPayment() {
|
|
2469
|
+
let saved = pending();
|
|
2470
|
+
const client = live(saved.value).client;
|
|
2471
|
+
if (!saved.value.approval || terminal(saved.value.status))
|
|
2472
|
+
fail3("WALLET_PAYMENT_NOT_APPROVED", "Retrieve fresh owner approval for this exact payment before submission.");
|
|
2473
|
+
if (!saved.value.submissionStarted && saved.value.operation.expiresAt <= now())
|
|
2474
|
+
fail3("WALLET_PAYMENT_EXPIRED", "This approval expired before submission. Preserve the payment for recovery.");
|
|
2475
|
+
saved = save({ ...saved.value, submissionStarted: true, status: "submitting" }, saved.encoded);
|
|
2476
|
+
try {
|
|
2477
|
+
const result = await client.smartAccounts().submitUserOperation(saved.value.operation.id, saved.value.approval.signature, saved.value.submissionKey);
|
|
2478
|
+
return status(receiveOperation(result, saved).value);
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
saved = save({ ...saved.value, status: "unknown" }, saved.encoded);
|
|
2481
|
+
if (error instanceof RestClientError && error.status !== void 0 && error.status >= 400) await retire(saved).catch(() => void 0);
|
|
2482
|
+
throw error;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
function retire(saved) {
|
|
2486
|
+
return live(saved.value).client.smartAccounts().retireUserOperation(saved.value.operation.id).then((result) => receiveOperation(result, saved));
|
|
2487
|
+
}
|
|
2488
|
+
async function retirePayment() {
|
|
2489
|
+
const saved = pending();
|
|
2490
|
+
if (!saved.value.approval || terminal(saved.value.status)) fail3("WALLET_PAYMENT_NOT_APPROVED", "Only an approved payment that has not settled can be retired.");
|
|
2491
|
+
return status((await retire(saved)).value);
|
|
2492
|
+
}
|
|
2493
|
+
const revisions = /* @__PURE__ */ new Map();
|
|
2494
|
+
async function refreshPayment(options2 = {}) {
|
|
2495
|
+
if (options2.waitSeconds !== void 0 && (!Number.isInteger(options2.waitSeconds) || options2.waitSeconds < 1 || options2.waitSeconds > 20))
|
|
2496
|
+
fail3("WALLET_PAYMENT_INPUT_INVALID", "Wait between one and twenty seconds.");
|
|
2497
|
+
const saved = pending(), client = live(saved.value).client;
|
|
2498
|
+
try {
|
|
2499
|
+
if (saved.value.submissionStarted) {
|
|
2500
|
+
const since = revisions.get(saved.value.operation.id);
|
|
2501
|
+
const wait = options2.waitSeconds !== void 0 && since !== void 0 ? { seconds: options2.waitSeconds, since } : void 0;
|
|
2502
|
+
return status(receiveOperation(await client.smartAccounts().userOperation(saved.value.operation.id, wait), saved).value);
|
|
2503
|
+
}
|
|
2504
|
+
if (!saved.value.review) return status((await requestReview(saved)).value);
|
|
2505
|
+
if (saved.value.approval) {
|
|
2506
|
+
const [value2, operation] = await Promise.all([
|
|
2507
|
+
client.request({ requestTarget: reviewPath + "/" + saved.value.review.id }),
|
|
2508
|
+
client.smartAccounts().userOperation(saved.value.operation.id)
|
|
2509
|
+
]);
|
|
2510
|
+
const reviewed = receiveView(value2, saved, true);
|
|
2511
|
+
if (!snapshot2(operation).submission && operation.state !== "retired") return status(reviewed.value);
|
|
2512
|
+
return status(receiveOperation(operation, save({ ...reviewed.value, submissionStarted: true, status: "submitting" }, reviewed.encoded)).value);
|
|
2513
|
+
}
|
|
2514
|
+
const value = await client.request({ requestTarget: reviewPath + "/" + saved.value.review.id });
|
|
2515
|
+
return status(receiveView(value, saved, true).value);
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
if (error instanceof RestClientError && ["WALLET_PAYMENT_MISMATCH", "WALLET_PAYMENT_CHANGED", "WALLET_PAYMENT_STORAGE_UNAVAILABLE"].includes(error.code)) throw error;
|
|
2518
|
+
return status(save({ ...saved.value, status: "unknown" }, saved.encoded).value);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
function pendingPayment() {
|
|
2522
|
+
const value = read()?.value;
|
|
2523
|
+
return value ? status(value) : null;
|
|
2524
|
+
}
|
|
2525
|
+
function clearPayment() {
|
|
2526
|
+
const saved = pending(), archivedAtMs = now(), signing = saved.value.operation.signing;
|
|
2527
|
+
if (!("ownerProfile" in signing)) mismatch();
|
|
2528
|
+
const settled = terminal(saved.value.status);
|
|
2529
|
+
const unsignedWindowElapsed = saved.value.submissionStarted === false && !saved.value.approval && typeof signing.validUntil === "string" && /^[1-9][0-9]{0,14}$/.test(signing.validUntil) && BigInt(signing.validUntil) < 2n ** 48n && integer2(archivedAtMs) && BigInt(Math.floor(archivedAtMs / 1e3)) > BigInt(signing.validUntil);
|
|
2530
|
+
const validityElapsed = typeof signing.validUntil === "string" && /^[1-9][0-9]{0,14}$/.test(signing.validUntil) && BigInt(signing.validUntil) < 2n ** 48n && integer2(archivedAtMs) && BigInt(Math.floor(archivedAtMs / 1e3)) > BigInt(signing.validUntil);
|
|
2531
|
+
let grantGone = false;
|
|
2532
|
+
try {
|
|
2533
|
+
live(saved.value);
|
|
2534
|
+
} catch {
|
|
2535
|
+
grantGone = true;
|
|
2536
|
+
}
|
|
2537
|
+
if (!settled && !unsignedWindowElapsed && !(grantGone && validityElapsed))
|
|
2538
|
+
fail3("WALLET_PAYMENT_UNRESOLVED", "Keep this payment until its outcome is verified or its original approval window has elapsed without a saved approval or local submission.");
|
|
2539
|
+
const existing = raw(historyKey);
|
|
2540
|
+
try {
|
|
2541
|
+
const history = existing === null ? [] : JSON.parse(existing);
|
|
2542
|
+
if (!Array.isArray(history)) throw new Error();
|
|
2543
|
+
const { payFailure: _decoded, ...archived } = status(saved.value);
|
|
2544
|
+
const receipt = {
|
|
2545
|
+
...archived,
|
|
2546
|
+
...!settled ? { status: "unknown", archiveReason: unsignedWindowElapsed ? "no-local-submission-recorded" : "grant-expired" } : {},
|
|
2547
|
+
issuer,
|
|
2548
|
+
audience,
|
|
2549
|
+
callbackUri,
|
|
2550
|
+
archivedAtMs,
|
|
2551
|
+
grantId: saved.value.grant.id,
|
|
2552
|
+
grantIncarnation: saved.value.grant.incarnation,
|
|
2553
|
+
planId: saved.value.plan.id,
|
|
2554
|
+
planCommitment: saved.value.plan.commitment,
|
|
2555
|
+
operationCommitment: saved.value.operation.commitment,
|
|
2556
|
+
reviewState: saved.value.state,
|
|
2557
|
+
reviewKey: saved.value.reviewKey,
|
|
2558
|
+
submissionKey: saved.value.submissionKey,
|
|
2559
|
+
validAfter: signing.validAfter,
|
|
2560
|
+
validUntil: signing.validUntil
|
|
2561
|
+
};
|
|
2562
|
+
const receipts = [...history.filter((item) => item.operationId !== saved.value.operation.id), receipt];
|
|
2563
|
+
const fits = () => receipts.length <= 64 && new TextEncoder().encode(JSON.stringify(receipts)).length <= maximumBytes;
|
|
2564
|
+
while (!fits() && receipts.length > 1) receipts.shift();
|
|
2565
|
+
const encoded = JSON.stringify(receipts);
|
|
2566
|
+
if (!fits() || raw() !== saved.encoded || raw(historyKey) !== existing) throw new Error();
|
|
2567
|
+
storage.setItem(historyKey, encoded);
|
|
2568
|
+
if (storage.getItem(historyKey) !== encoded || raw() !== saved.encoded) throw new Error();
|
|
2569
|
+
storage.removeItem(key);
|
|
2570
|
+
if (storage.getItem(key) !== null) throw new Error();
|
|
2571
|
+
} catch {
|
|
2572
|
+
fail3("WALLET_PAYMENT_STORAGE_UNAVAILABLE", "The original payment must be archived before clearing this tab\u2019s pending record.");
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return Object.freeze({ preparePayment, reviewUrl, completePayment, submitPayment, refreshPayment, retirePayment, pendingPayment, clearPayment });
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/rest/wallet/grantLifetime.ts
|
|
2579
|
+
var walletAppGrantMaximumLifetimeSeconds = 7776e3;
|
|
2580
|
+
|
|
2581
|
+
// src/rest/client/wallet.ts
|
|
2582
|
+
var maximumBytes2 = 32 * 1024;
|
|
2583
|
+
var requestFields = [
|
|
2584
|
+
"version",
|
|
2585
|
+
"issuer",
|
|
2586
|
+
"origin",
|
|
2587
|
+
"callbackUri",
|
|
2588
|
+
"audience",
|
|
2589
|
+
"appGeneration",
|
|
2590
|
+
"requestKey",
|
|
2591
|
+
"state",
|
|
2592
|
+
"codeChallenge",
|
|
2593
|
+
"nonce",
|
|
2594
|
+
"issuedAtMs",
|
|
2595
|
+
"expiresAtMs"
|
|
2596
|
+
];
|
|
2597
|
+
function fail4(code, message2) {
|
|
2598
|
+
throw new RestClientError(code, message2);
|
|
2599
|
+
}
|
|
2600
|
+
function invalid6() {
|
|
2601
|
+
return fail4("WALLET_RESPONSE_INVALID", "Center returned an invalid wallet connection response.");
|
|
2602
|
+
}
|
|
2603
|
+
function object2(value, required2, optional2 = []) {
|
|
2604
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid6();
|
|
2605
|
+
const fields2 = Object.keys(value);
|
|
2606
|
+
if (required2.some((key) => !fields2.includes(key)) || fields2.some((key) => !required2.includes(key) && !optional2.includes(key))) invalid6();
|
|
2607
|
+
return value;
|
|
2608
|
+
}
|
|
2609
|
+
function integer3(value) {
|
|
2610
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2611
|
+
}
|
|
2612
|
+
function signature(value) {
|
|
2613
|
+
return typeof value === "string" && /^0x[0-9a-f]{130}$/.test(value);
|
|
2614
|
+
}
|
|
2615
|
+
function canonicalOrigin(value) {
|
|
2616
|
+
if (typeof value !== "string" || value.length > 512 || /[^\x21-\x7e]|[*\\%?#]/.test(value)) invalid6();
|
|
2617
|
+
let url;
|
|
2618
|
+
try {
|
|
2619
|
+
url = new URL(value);
|
|
2620
|
+
} catch {
|
|
2621
|
+
return invalid6();
|
|
2622
|
+
}
|
|
2623
|
+
if (url.origin !== value || url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) invalid6();
|
|
2624
|
+
return value;
|
|
2625
|
+
}
|
|
2626
|
+
function canonicalCallback(value, origin) {
|
|
2627
|
+
if (typeof value !== "string" || value.length > 2048 || /[^\x21-\x7e]|[*\\%?#]/.test(value)) invalid6();
|
|
2628
|
+
let url;
|
|
2629
|
+
try {
|
|
2630
|
+
url = new URL(value);
|
|
2631
|
+
} catch {
|
|
2632
|
+
return invalid6();
|
|
2633
|
+
}
|
|
2634
|
+
if (url.href !== value || url.origin !== origin || url.username || url.password || url.pathname.includes("//")) invalid6();
|
|
2635
|
+
return value;
|
|
2636
|
+
}
|
|
2637
|
+
function randomToken() {
|
|
2638
|
+
const bytes3 = crypto.getRandomValues(new Uint8Array(32));
|
|
2639
|
+
return btoa(String.fromCharCode(...bytes3)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
2640
|
+
}
|
|
2641
|
+
function createCenterWalletClient(options) {
|
|
2642
|
+
let issuer, audience, callbackUri, origin;
|
|
2643
|
+
try {
|
|
2644
|
+
issuer = canonicalOrigin(options.issuer);
|
|
2645
|
+
audience = canonicalOrigin(options.audience);
|
|
2646
|
+
origin = canonicalOrigin(new URL(options.callbackUri).origin);
|
|
2647
|
+
callbackUri = canonicalCallback(options.callbackUri, origin);
|
|
2648
|
+
} catch {
|
|
2649
|
+
return fail4("WALLET_CONFIG_INVALID", "Use fixed canonical Center origins and an exact app callback URI.");
|
|
2650
|
+
}
|
|
2651
|
+
const now = options.now ?? Date.now, transport = options.fetch ?? globalThis.fetch;
|
|
2652
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2653
|
+
if (!integer3(timeoutMs) || timeoutMs > 6e4) fail4("WALLET_CONFIG_INVALID", "Use a wallet request timeout from 1 to 60000 milliseconds.");
|
|
2654
|
+
const location = options.location ?? {
|
|
2655
|
+
href: () => globalThis.location.href,
|
|
2656
|
+
replace: (url) => globalThis.history.replaceState(null, "", url)
|
|
2657
|
+
};
|
|
2658
|
+
const storageKey = "center.wallet.connection.v1:" + issuer + ":" + callbackUri;
|
|
2659
|
+
let disconnectVersion = 0;
|
|
2660
|
+
function clock() {
|
|
2661
|
+
const value = now();
|
|
2662
|
+
if (!integer3(value)) fail4("WALLET_CONFIG_INVALID", "The wallet connection clock is invalid.");
|
|
2663
|
+
return value;
|
|
2664
|
+
}
|
|
2665
|
+
function storage() {
|
|
2666
|
+
try {
|
|
2667
|
+
return options.storage ?? globalThis.sessionStorage;
|
|
2668
|
+
} catch {
|
|
2669
|
+
return fail4("WALLET_STORAGE_UNAVAILABLE", "This tab needs session storage to keep its wallet connection.");
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
function raw() {
|
|
2673
|
+
try {
|
|
2674
|
+
return storage().getItem(storageKey);
|
|
2675
|
+
} catch {
|
|
2676
|
+
return fail4("WALLET_STORAGE_UNAVAILABLE", "This tab could not read its wallet connection.");
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
function save(value, expected) {
|
|
2680
|
+
const encoded = JSON.stringify(value);
|
|
2681
|
+
if (raw() !== expected) fail4("WALLET_HANDOFF_CHANGED", "This tab changed its wallet connection. Resume the current connection.");
|
|
2682
|
+
try {
|
|
2683
|
+
if (new TextEncoder().encode(encoded).length > maximumBytes2) throw new Error();
|
|
2684
|
+
storage().setItem(storageKey, encoded);
|
|
2685
|
+
if (storage().getItem(storageKey) !== encoded) throw new Error();
|
|
2686
|
+
} catch {
|
|
2687
|
+
return fail4("WALLET_STORAGE_UNAVAILABLE", "This tab could not preserve its wallet connection.");
|
|
2688
|
+
}
|
|
2689
|
+
return encoded;
|
|
2690
|
+
}
|
|
2691
|
+
function checkRequest(value) {
|
|
2692
|
+
const r = object2(value, requestFields);
|
|
2693
|
+
if (r.version !== "center-wallet-handoff-request-v1" || r.issuer !== issuer || r.audience !== audience || r.origin !== origin || r.callbackUri !== callbackUri || !integer3(r.appGeneration) || !integer3(r.issuedAtMs) || !integer3(r.expiresAtMs) || r.expiresAtMs <= r.issuedAtMs || r.expiresAtMs - r.issuedAtMs > 9e5 || typeof r.requestKey !== "string" || !/^0x[0-9a-f]{40}$/.test(r.requestKey) || BigInt(r.requestKey) <= 1n || typeof r.nonce !== "string" || !/^0x[0-9a-f]{64}$/.test(r.nonce)) invalid6();
|
|
2694
|
+
try {
|
|
2695
|
+
validateWalletHandoffToken(r.state);
|
|
2696
|
+
validateWalletHandoffToken(r.codeChallenge);
|
|
2697
|
+
} catch {
|
|
2698
|
+
invalid6();
|
|
2699
|
+
}
|
|
2700
|
+
return r;
|
|
2701
|
+
}
|
|
2702
|
+
function checkIntent(value, request) {
|
|
2703
|
+
const intent = object2(value, ["id", "request", "state", "createdAtMs", "expiresAtMs"]);
|
|
2704
|
+
try {
|
|
2705
|
+
validateWalletHandoffToken(intent.id);
|
|
2706
|
+
} catch {
|
|
2707
|
+
invalid6();
|
|
2708
|
+
}
|
|
2709
|
+
const returned = checkRequest(intent.request);
|
|
2710
|
+
if (requestFields.some((field) => returned[field] !== request[field]) || intent.state !== "prepared" || !integer3(intent.createdAtMs) || intent.createdAtMs > clock() + 3e4 || intent.createdAtMs < request.issuedAtMs - 3e4 || intent.expiresAtMs !== request.expiresAtMs || intent.createdAtMs >= request.expiresAtMs) invalid6();
|
|
2711
|
+
return intent;
|
|
2712
|
+
}
|
|
2713
|
+
function checkGrant(value, request) {
|
|
2714
|
+
const g = object2(value, [
|
|
2715
|
+
"kind",
|
|
2716
|
+
"id",
|
|
2717
|
+
"incarnation",
|
|
2718
|
+
"accountId",
|
|
2719
|
+
"signerAddress",
|
|
2720
|
+
"scopes",
|
|
2721
|
+
"origin",
|
|
2722
|
+
"callbackUri",
|
|
2723
|
+
"audience",
|
|
2724
|
+
"appGeneration",
|
|
2725
|
+
"authorityEpoch",
|
|
2726
|
+
"sessionEpoch",
|
|
2727
|
+
"createdAt",
|
|
2728
|
+
"expiresAt",
|
|
2729
|
+
"revokedAt",
|
|
2730
|
+
"retainUntil"
|
|
2731
|
+
]);
|
|
2732
|
+
const epoch = (value2) => typeof value2 === "string" && /^[1-9][0-9]{0,18}$/.test(value2) && BigInt(value2) <= 9223372036854775807n;
|
|
2733
|
+
if (g.kind !== "wallet-app" || typeof g.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(g.id) || !epoch(g.incarnation) || !epoch(g.authorityEpoch) || !epoch(g.sessionEpoch) || typeof g.accountId !== "string" || !/^eip155:8453:0x[0-9a-f]{40}$/.test(g.accountId) || BigInt(g.accountId.slice(12)) <= 1n || g.signerAddress !== request.requestKey || g.signerAddress === g.accountId.slice(12) || !Array.isArray(g.scopes) || g.scopes.length !== 3 || g.scopes.join(",") !== "read,plan,relay" || g.origin !== origin || g.callbackUri !== callbackUri || g.audience !== audience || g.appGeneration !== request.appGeneration || !integer3(g.createdAt) || g.createdAt > Math.floor(clock() / 1e3) + 30 || !integer3(g.expiresAt) || g.expiresAt <= g.createdAt || g.expiresAt - g.createdAt > walletAppGrantMaximumLifetimeSeconds || g.revokedAt !== null || !integer3(g.retainUntil) || g.retainUntil !== g.expiresAt + 86400) invalid6();
|
|
2734
|
+
return g;
|
|
2735
|
+
}
|
|
2736
|
+
function read() {
|
|
2737
|
+
const encoded = raw();
|
|
2738
|
+
if (encoded === null) return null;
|
|
2739
|
+
try {
|
|
2740
|
+
if (new TextEncoder().encode(encoded).length > maximumBytes2) invalid6();
|
|
2741
|
+
const v = object2(JSON.parse(encoded), ["version", "key", "request", "signature"], ["verifier", "intent", "exchange", "grant"]);
|
|
2742
|
+
if (v.version !== "center-wallet-client-v1" || typeof v.key !== "string" || !/^0x[0-9a-f]{64}$/.test(v.key) || !signature(v.signature)) invalid6();
|
|
2743
|
+
const request = checkRequest(v.request);
|
|
2744
|
+
if (privateKeyToAccount3(v.key).address.toLowerCase() !== request.requestKey) invalid6();
|
|
2745
|
+
if (v.grant === void 0 || v.verifier !== void 0) {
|
|
2746
|
+
if (walletHandoffPkceChallenge(v.verifier) !== request.codeChallenge) invalid6();
|
|
2747
|
+
}
|
|
2748
|
+
if (v.intent !== void 0) checkIntent(v.intent, request);
|
|
2749
|
+
if (v.exchange !== void 0) {
|
|
2750
|
+
const exchange2 = object2(v.exchange, ["code"], ["signature"]);
|
|
2751
|
+
if (!v.intent || typeof v.verifier !== "string" || exchange2.signature !== void 0 && !signature(exchange2.signature)) invalid6();
|
|
2752
|
+
validateWalletHandoffToken(exchange2.code);
|
|
2753
|
+
}
|
|
2754
|
+
if (v.grant !== void 0) {
|
|
2755
|
+
if (!v.intent) invalid6();
|
|
2756
|
+
checkGrant(v.grant, request);
|
|
2757
|
+
}
|
|
2758
|
+
return { value: v, encoded };
|
|
2759
|
+
} catch {
|
|
2760
|
+
return fail4("WALLET_STORAGE_INVALID", "This tab has an invalid saved wallet connection. Disconnect it before starting again.");
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
async function json(path, body) {
|
|
2764
|
+
const controller = new AbortController();
|
|
2765
|
+
const monotonicNow = () => globalThis.performance?.now() ?? Date.now(), deadlineAt = monotonicNow() + timeoutMs;
|
|
2766
|
+
const checkDeadline = () => {
|
|
2767
|
+
if (controller.signal.aborted || monotonicNow() >= deadlineAt) {
|
|
2768
|
+
controller.abort();
|
|
2769
|
+
fail4("WALLET_NETWORK_ERROR", "The wallet request timed out. Retry the pending connection.");
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
let timer;
|
|
2773
|
+
const deadline = new Promise((_, reject) => {
|
|
2774
|
+
timer = setTimeout(() => {
|
|
2775
|
+
controller.abort();
|
|
2776
|
+
reject(new RestClientError("WALLET_NETWORK_ERROR", "The wallet request timed out. Retry the pending connection."));
|
|
2777
|
+
}, timeoutMs);
|
|
2778
|
+
});
|
|
2779
|
+
const execute = async () => {
|
|
2780
|
+
const response = await transport(issuer + path, {
|
|
2781
|
+
method: body === void 0 ? "GET" : "POST",
|
|
2782
|
+
credentials: "omit",
|
|
2783
|
+
redirect: "error",
|
|
2784
|
+
cache: "no-store",
|
|
2785
|
+
signal: controller.signal,
|
|
2786
|
+
headers: { accept: "application/json", "x-center-wallet-request": "1", ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
2787
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
2788
|
+
});
|
|
2789
|
+
if (controller.signal.aborted || monotonicNow() >= deadlineAt) {
|
|
2790
|
+
void response.body?.cancel().catch(() => {
|
|
2791
|
+
});
|
|
2792
|
+
checkDeadline();
|
|
2793
|
+
}
|
|
2794
|
+
if (response.redirected || response.url && response.url !== issuer + path) {
|
|
2795
|
+
void response.body?.cancel().catch(() => {
|
|
2796
|
+
});
|
|
2797
|
+
invalid6();
|
|
2798
|
+
}
|
|
2799
|
+
if (!response.ok) {
|
|
2800
|
+
void response.body?.cancel().catch(() => {
|
|
2801
|
+
});
|
|
2802
|
+
if (response.status === 429 || response.status >= 500)
|
|
2803
|
+
return fail4("WALLET_NETWORK_ERROR", "The wallet request could not complete. Retry the pending connection.");
|
|
2804
|
+
return fail4("WALLET_REQUEST_REJECTED", "Center rejected the wallet request. The pending connection has been preserved.");
|
|
2805
|
+
}
|
|
2806
|
+
if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") ?? "") || Number(response.headers.get("content-length")) > maximumBytes2 || !response.body) {
|
|
2807
|
+
void response.body?.cancel().catch(() => {
|
|
2808
|
+
});
|
|
2809
|
+
invalid6();
|
|
2810
|
+
}
|
|
2811
|
+
const reader = response.body.getReader(), chunks = [];
|
|
2812
|
+
const cancel = () => {
|
|
2813
|
+
void reader.cancel().catch(() => {
|
|
2814
|
+
});
|
|
2815
|
+
};
|
|
2816
|
+
controller.signal.addEventListener("abort", cancel, { once: true });
|
|
2817
|
+
let size3 = 0;
|
|
2818
|
+
try {
|
|
2819
|
+
while (true) {
|
|
2820
|
+
const part = await reader.read();
|
|
2821
|
+
checkDeadline();
|
|
2822
|
+
if (part.done) break;
|
|
2823
|
+
size3 += part.value.length;
|
|
2824
|
+
if (size3 > maximumBytes2) invalid6();
|
|
2825
|
+
chunks.push(part.value);
|
|
2826
|
+
}
|
|
2827
|
+
const bytes3 = new Uint8Array(size3);
|
|
2828
|
+
let offset = 0;
|
|
2829
|
+
for (const chunk of chunks) {
|
|
2830
|
+
bytes3.set(chunk, offset);
|
|
2831
|
+
offset += chunk.length;
|
|
2832
|
+
}
|
|
2833
|
+
let decoded;
|
|
2834
|
+
try {
|
|
2835
|
+
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes3));
|
|
2836
|
+
} catch {
|
|
2837
|
+
return invalid6();
|
|
2838
|
+
}
|
|
2839
|
+
checkDeadline();
|
|
2840
|
+
return decoded;
|
|
2841
|
+
} finally {
|
|
2842
|
+
controller.signal.removeEventListener("abort", cancel);
|
|
2843
|
+
cancel();
|
|
2844
|
+
reader.releaseLock();
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
try {
|
|
2848
|
+
return await Promise.race([execute(), deadline]);
|
|
2849
|
+
} catch (error) {
|
|
2850
|
+
if (error instanceof RestClientError) throw error;
|
|
2851
|
+
return fail4("WALLET_NETWORK_ERROR", "The wallet request could not complete. Retry the pending connection.");
|
|
2852
|
+
} finally {
|
|
2853
|
+
clearTimeout(timer);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
async function prepared(intent) {
|
|
2857
|
+
const pending = read();
|
|
2858
|
+
if (!pending?.value.intent || pending.value.intent.id !== intent.id || pending.value.exchange || pending.value.grant)
|
|
2859
|
+
fail4("WALLET_HANDOFF_CHANGED", "This tab changed its wallet connection. Start again from the app.");
|
|
2860
|
+
const signature2 = await privateKeyToAccount3(pending.value.key).signTypedData(walletHandoffLaunchDocument({ request: intent.request, intentId: intent.id }));
|
|
2861
|
+
if (raw() !== pending.encoded) fail4("WALLET_HANDOFF_CHANGED", "This tab changed its wallet connection. Start again from the app.");
|
|
2862
|
+
return Object.freeze({
|
|
2863
|
+
intentId: intent.id,
|
|
2864
|
+
authorizationUrl: issuer + "/wallet?intent=" + intent.id,
|
|
2865
|
+
expiresAtMs: intent.expiresAtMs,
|
|
2866
|
+
launch(launchOptions) {
|
|
2867
|
+
const target = launchOptions?.target ?? "_self";
|
|
2868
|
+
if (target !== "_self" && !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(target))
|
|
2869
|
+
fail4("WALLET_LAUNCH_UNAVAILABLE", "Launch into this tab or a plainly named window the app opened.");
|
|
2870
|
+
if (raw() !== pending.encoded || intent.expiresAtMs <= clock())
|
|
2871
|
+
fail4("WALLET_HANDOFF_CHANGED", "This tab changed or expired its wallet connection. Start again from the app.");
|
|
2872
|
+
if (globalThis.location?.origin !== origin || !globalThis.document?.body)
|
|
2873
|
+
fail4("WALLET_LAUNCH_UNAVAILABLE", "Open the wallet connection from its original app tab.");
|
|
2874
|
+
const form = document.createElement("form");
|
|
2875
|
+
form.method = "POST";
|
|
2876
|
+
form.action = issuer + "/wallet/launch";
|
|
2877
|
+
form.target = target;
|
|
2878
|
+
form.hidden = true;
|
|
2879
|
+
for (const [name, value] of Object.entries({ intentId: intent.id, signature: signature2 })) {
|
|
2880
|
+
const input = document.createElement("input");
|
|
2881
|
+
input.type = "hidden";
|
|
2882
|
+
input.name = name;
|
|
2883
|
+
input.value = value;
|
|
2884
|
+
form.append(input);
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
document.body.append(form);
|
|
2888
|
+
HTMLFormElement.prototype.submit.call(form);
|
|
2889
|
+
} catch {
|
|
2890
|
+
fail4("WALLET_LAUNCH_UNAVAILABLE", "The wallet connection could not open. Try again from the app.");
|
|
2891
|
+
} finally {
|
|
2892
|
+
form.remove();
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
function connection(saved) {
|
|
2898
|
+
const grant = saved.grant;
|
|
2899
|
+
const signer = privateKeyToAccount3(saved.key);
|
|
2900
|
+
const ensureActive = () => {
|
|
2901
|
+
const current = read()?.value;
|
|
2902
|
+
if (!current?.grant || current.key !== saved.key || current.grant.id !== grant.id || current.grant.incarnation !== grant.incarnation || current.grant.accountId !== grant.accountId || current.grant.expiresAt !== grant.expiresAt || grant.expiresAt <= Math.floor(clock() / 1e3))
|
|
2903
|
+
fail4("WALLET_CONNECTION_INACTIVE", "This wallet connection is disconnected, expired or replaced.");
|
|
2904
|
+
};
|
|
2905
|
+
const config = {
|
|
2906
|
+
audience,
|
|
2907
|
+
accountId: grant.accountId,
|
|
2908
|
+
grantId: grant.id,
|
|
2909
|
+
signer: {
|
|
2910
|
+
address: signer.address,
|
|
2911
|
+
signTypedData: async (document2) => {
|
|
2912
|
+
ensureActive();
|
|
2913
|
+
const signature2 = document2.primaryType === "CenterRequest" ? await signer.signTypedData(document2) : await signer.signTypedData(document2);
|
|
2914
|
+
ensureActive();
|
|
2915
|
+
return signature2;
|
|
2916
|
+
}
|
|
2917
|
+
},
|
|
2918
|
+
fetch: (input, init) => {
|
|
2919
|
+
ensureActive();
|
|
2920
|
+
return transport(input, init);
|
|
2921
|
+
},
|
|
2922
|
+
now: () => Math.floor(clock() / 1e3),
|
|
2923
|
+
timeoutMs
|
|
2924
|
+
};
|
|
2925
|
+
return Object.freeze({
|
|
2926
|
+
accountId: grant.accountId,
|
|
2927
|
+
address: grant.accountId.slice(12),
|
|
2928
|
+
chainId: 8453,
|
|
2929
|
+
expiresAt: grant.expiresAt,
|
|
2930
|
+
capabilities: Object.freeze(["read", "plan", "relay"]),
|
|
2931
|
+
client: new CenterClient(config)
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
async function prepareConnection() {
|
|
2935
|
+
const startingVersion = disconnectVersion;
|
|
2936
|
+
let pending = read();
|
|
2937
|
+
if (pending?.value.exchange) fail4("WALLET_HANDOFF_PENDING", "Recover the pending exchange before starting another connection.");
|
|
2938
|
+
if (pending?.value.grant) fail4("WALLET_ALREADY_CONNECTED", "Disconnect the current wallet connection before replacing it.");
|
|
2939
|
+
if (pending && pending.value.request.expiresAtMs <= clock()) fail4("WALLET_HANDOFF_EXPIRED", "The connection request expired. Disconnect it before starting again.");
|
|
2940
|
+
if (pending?.value.intent) return prepared(pending.value.intent);
|
|
2941
|
+
if (!pending) {
|
|
2942
|
+
const config = object2(await json("/wallet/config?appOrigin=" + encodeURIComponent(origin)), ["version", "issuer", "audience", "rpId", "app"]);
|
|
2943
|
+
const app = object2(config.app, ["origin", "callbackUris", "generation"]);
|
|
2944
|
+
if (config.version !== "center-wallet-v1" || config.issuer !== issuer || config.audience !== audience || config.rpId !== new URL(issuer).hostname || app.origin !== origin || !integer3(app.generation) || !Array.isArray(app.callbackUris) || app.callbackUris.length > 4 || !app.callbackUris.includes(callbackUri) || new Set(app.callbackUris).size !== app.callbackUris.length) invalid6();
|
|
2945
|
+
app.callbackUris.forEach((uri) => canonicalCallback(uri, origin));
|
|
2946
|
+
const key = generatePrivateKey(), signer = privateKeyToAccount3(key), verifier = randomToken(), issuedAtMs = clock();
|
|
2947
|
+
const request = {
|
|
2948
|
+
version: "center-wallet-handoff-request-v1",
|
|
2949
|
+
issuer,
|
|
2950
|
+
audience,
|
|
2951
|
+
origin,
|
|
2952
|
+
callbackUri,
|
|
2953
|
+
appGeneration: app.generation,
|
|
2954
|
+
requestKey: signer.address.toLowerCase(),
|
|
2955
|
+
state: randomToken(),
|
|
2956
|
+
codeChallenge: walletHandoffPkceChallenge(verifier),
|
|
2957
|
+
nonce: toHex7(crypto.getRandomValues(new Uint8Array(32))),
|
|
2958
|
+
issuedAtMs,
|
|
2959
|
+
expiresAtMs: issuedAtMs + 9e5
|
|
2960
|
+
};
|
|
2961
|
+
const value = { version: "center-wallet-client-v1", key, verifier, request, signature: await signer.signTypedData(walletHandoffRequestDocument(request)) };
|
|
2962
|
+
if (startingVersion !== disconnectVersion) fail4("WALLET_HANDOFF_CHANGED", "This tab disconnected its wallet connection.");
|
|
2963
|
+
pending = { value, encoded: save(value, null) };
|
|
2964
|
+
}
|
|
2965
|
+
const intent = checkIntent(await json("/wallet/handoff/prepare", { request: pending.value.request, signature: pending.value.signature }), pending.value.request);
|
|
2966
|
+
save({ ...pending.value, intent }, pending.encoded);
|
|
2967
|
+
return prepared(intent);
|
|
2968
|
+
}
|
|
2969
|
+
async function exchange(pending) {
|
|
2970
|
+
let value = pending.value;
|
|
2971
|
+
if (!value.intent || !value.exchange || !value.verifier) fail4("WALLET_HANDOFF_MISSING", "There is no pending wallet exchange in this tab.");
|
|
2972
|
+
if (!value.exchange.signature) {
|
|
2973
|
+
const signature2 = await privateKeyToAccount3(value.key).signTypedData(walletHandoffExchangeDocument({
|
|
2974
|
+
request: value.request,
|
|
2975
|
+
intentId: value.intent.id,
|
|
2976
|
+
codeHash: walletHandoffCodeHash(value.exchange.code)
|
|
2977
|
+
}));
|
|
2978
|
+
value = { ...value, exchange: { code: value.exchange.code, signature: signature2 } };
|
|
2979
|
+
pending = { value, encoded: save(value, pending.encoded) };
|
|
2980
|
+
}
|
|
2981
|
+
const intent = value.intent, proof = value.exchange;
|
|
2982
|
+
const response = object2(await json("/wallet/handoff/exchange", {
|
|
2983
|
+
intentId: intent.id,
|
|
2984
|
+
request: value.request,
|
|
2985
|
+
code: proof.code,
|
|
2986
|
+
verifier: value.verifier,
|
|
2987
|
+
signature: proof.signature
|
|
2988
|
+
}), ["grant", "replayed"]);
|
|
2989
|
+
if (typeof response.replayed !== "boolean") invalid6();
|
|
2990
|
+
const grant = checkGrant(response.grant, value.request);
|
|
2991
|
+
if (grant.expiresAt <= Math.floor(clock() / 1e3)) invalid6();
|
|
2992
|
+
const connected = { version: value.version, key: value.key, request: value.request, signature: value.signature, intent, grant };
|
|
2993
|
+
save(connected, pending.encoded);
|
|
2994
|
+
return connection(connected);
|
|
2995
|
+
}
|
|
2996
|
+
async function completeConnection(callbackUrl) {
|
|
2997
|
+
const incoming = callbackUrl ?? location.href();
|
|
2998
|
+
try {
|
|
2999
|
+
if (location.href().slice(0, callbackUri.length) === callbackUri) location.replace(callbackUri);
|
|
3000
|
+
} catch {
|
|
3001
|
+
return fail4("WALLET_CALLBACK_INVALID", "The wallet callback could not be cleared safely.");
|
|
3002
|
+
}
|
|
3003
|
+
let pending = read();
|
|
3004
|
+
if (!pending?.value.intent || pending.value.grant) fail4("WALLET_CALLBACK_INVALID", "There is no matching wallet callback pending in this tab.");
|
|
3005
|
+
let code;
|
|
3006
|
+
try {
|
|
3007
|
+
if (typeof incoming !== "string" || incoming.length > 4096) throw new Error();
|
|
3008
|
+
const url = new URL(incoming), entries = [...url.searchParams];
|
|
3009
|
+
if (incoming.slice(0, incoming.indexOf("?")) !== callbackUri || url.origin + url.pathname !== callbackUri || url.username || url.password || url.hash || entries.length !== 3 || ["code", "state", "iss"].some((name) => url.searchParams.getAll(name).length !== 1) || url.searchParams.get("state") !== pending.value.request.state || url.searchParams.get("iss") !== issuer) throw new Error();
|
|
3010
|
+
code = validateWalletHandoffToken(url.searchParams.get("code"));
|
|
3011
|
+
if (pending.value.exchange && pending.value.exchange.code !== code) throw new Error();
|
|
3012
|
+
} catch {
|
|
3013
|
+
return fail4("WALLET_CALLBACK_INVALID", "The wallet callback does not match this tab and configured issuer.");
|
|
3014
|
+
}
|
|
3015
|
+
if (!pending.value.exchange) {
|
|
3016
|
+
const value = { ...pending.value, exchange: { code } };
|
|
3017
|
+
pending = { value, encoded: save(value, pending.encoded) };
|
|
3018
|
+
}
|
|
3019
|
+
return exchange(pending);
|
|
3020
|
+
}
|
|
3021
|
+
function restoreConnection() {
|
|
3022
|
+
const saved = read()?.value;
|
|
3023
|
+
return saved?.grant && saved.grant.expiresAt > Math.floor(clock() / 1e3) ? connection(saved) : null;
|
|
3024
|
+
}
|
|
3025
|
+
async function retryConnection() {
|
|
3026
|
+
const pending = read();
|
|
3027
|
+
if (pending?.value.grant) {
|
|
3028
|
+
const restored = restoreConnection();
|
|
3029
|
+
if (restored) return restored;
|
|
3030
|
+
fail4("WALLET_CONNECTION_EXPIRED", "The wallet connection expired. Disconnect it before starting again.");
|
|
3031
|
+
}
|
|
3032
|
+
if (!pending?.value.exchange) fail4("WALLET_HANDOFF_MISSING", "There is no pending wallet exchange in this tab.");
|
|
3033
|
+
return exchange(pending);
|
|
3034
|
+
}
|
|
3035
|
+
function disconnect() {
|
|
3036
|
+
disconnectVersion++;
|
|
3037
|
+
try {
|
|
3038
|
+
storage().removeItem(storageKey);
|
|
3039
|
+
if (storage().getItem(storageKey) !== null) throw new Error();
|
|
3040
|
+
} catch {
|
|
3041
|
+
fail4("WALLET_STORAGE_UNAVAILABLE", "This tab could not clear its wallet connection.");
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
function payments() {
|
|
3045
|
+
return createCenterWalletPaymentClient({
|
|
3046
|
+
issuer,
|
|
3047
|
+
audience,
|
|
3048
|
+
callbackUri,
|
|
3049
|
+
location,
|
|
3050
|
+
now: clock,
|
|
3051
|
+
storage: { getItem: (key) => storage().getItem(key), setItem: (key, value) => storage().setItem(key, value), removeItem: (key) => storage().removeItem(key) },
|
|
3052
|
+
connection: () => {
|
|
3053
|
+
const saved = read()?.value;
|
|
3054
|
+
return saved?.grant && saved.grant.expiresAt > Math.floor(clock() / 1e3) ? { grant: structuredClone(saved.grant), client: connection(saved).client } : null;
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
return Object.freeze({ prepareConnection, completeConnection, retryConnection, restoreConnection, disconnect, payments });
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// src/rest/userOperations/sponsoredCalls.ts
|
|
3062
|
+
import { keccak256 as keccak25612, stringToHex as stringToHex7 } from "viem";
|
|
3063
|
+
function sponsoredCallsCommitment(calls) {
|
|
3064
|
+
return keccak25612(stringToHex7(JSON.stringify(calls.map((call) => [call.chainId, call.to.toLowerCase(), call.data.toLowerCase(), BigInt(call.value).toString()]))));
|
|
3065
|
+
}
|
|
3066
|
+
export {
|
|
3067
|
+
RestClientError,
|
|
3068
|
+
assertReviewedOperation,
|
|
3069
|
+
createCenterWalletClient,
|
|
3070
|
+
ownerOperationSignature,
|
|
3071
|
+
ownerOperationSigning,
|
|
3072
|
+
sponsoredCallsCommitment,
|
|
3073
|
+
userOperationMaximumCost
|
|
3074
|
+
};
|