@gasfree-kit/evm-4337 0.1.0 → 0.2.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 +338 -0
- package/dist/index.d.mts +161 -14
- package/dist/index.d.ts +161 -14
- package/dist/index.js +819 -92
- package/dist/index.mjs +804 -86
- package/package.json +15 -3
- package/scripts/postinstall.js +44 -23
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,16 @@ var DEFAULT_ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
|
|
|
4
4
|
var DEFAULT_SAFE_MODULES_VERSION = "0.3.0";
|
|
5
5
|
var DEFAULT_PAYMASTER_ADDRESS = "0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba";
|
|
6
6
|
var DEFAULT_TRANSFER_MAX_FEE = 1e15;
|
|
7
|
+
function getNormalizedSponsorshipPolicyId(config) {
|
|
8
|
+
const sponsorshipPolicyId = config.sponsorshipPolicyId?.trim() ?? "";
|
|
9
|
+
if (config.isSponsored && !sponsorshipPolicyId) {
|
|
10
|
+
throw new Error("sponsorshipPolicyId is required when isSponsored is true");
|
|
11
|
+
}
|
|
12
|
+
return sponsorshipPolicyId;
|
|
13
|
+
}
|
|
7
14
|
function getErc4337ConfigForChain(config) {
|
|
8
15
|
const chainConfig = EVM_CHAINS[config.chain];
|
|
16
|
+
const sponsorshipPolicyId = getNormalizedSponsorshipPolicyId(config);
|
|
9
17
|
return {
|
|
10
18
|
chainId: chainConfig.chainId,
|
|
11
19
|
provider: config.rpcUrl,
|
|
@@ -16,7 +24,7 @@ function getErc4337ConfigForChain(config) {
|
|
|
16
24
|
safeModulesVersion: config.safeModulesVersion ?? DEFAULT_SAFE_MODULES_VERSION,
|
|
17
25
|
transferMaxFee: DEFAULT_TRANSFER_MAX_FEE,
|
|
18
26
|
isSponsored: config.isSponsored,
|
|
19
|
-
sponsorshipPolicyId
|
|
27
|
+
sponsorshipPolicyId,
|
|
20
28
|
paymasterToken: {
|
|
21
29
|
address: config.paymasterTokenAddress ?? chainConfig.usdtAddress
|
|
22
30
|
}
|
|
@@ -28,7 +36,8 @@ var GAS_FEE_FALLBACKS = {
|
|
|
28
36
|
polygon: 100000n,
|
|
29
37
|
celo: 50000n,
|
|
30
38
|
optimism: 100000n,
|
|
31
|
-
base: 150000n
|
|
39
|
+
base: 150000n,
|
|
40
|
+
plasma: 50000n
|
|
32
41
|
};
|
|
33
42
|
var GAS_FEE_ESTIMATES = {
|
|
34
43
|
ethereum: "1.40",
|
|
@@ -36,11 +45,639 @@ var GAS_FEE_ESTIMATES = {
|
|
|
36
45
|
polygon: "0.10",
|
|
37
46
|
celo: "0.05",
|
|
38
47
|
optimism: "0.10",
|
|
39
|
-
base: "0.15"
|
|
48
|
+
base: "0.15",
|
|
49
|
+
plasma: "0.05"
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/passkey/linkPasskey.ts
|
|
53
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
54
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
55
|
+
import { TransactionFailedError as TransactionFailedError2 } from "@gasfree-kit/core";
|
|
56
|
+
|
|
57
|
+
// src/userOpUtils.ts
|
|
58
|
+
import { Bundler, SendUseroperationResponse } from "abstractionkit";
|
|
59
|
+
import { InsufficientBalanceError, TransactionFailedError } from "@gasfree-kit/core";
|
|
60
|
+
var USER_OP_TIMEOUT_MS = 12e4;
|
|
61
|
+
async function waitForUserOpConfirmation(userOpHash, bundlerUrl, entryPointAddress) {
|
|
62
|
+
const bundler = new Bundler(bundlerUrl);
|
|
63
|
+
const response = new SendUseroperationResponse(userOpHash, bundler, entryPointAddress);
|
|
64
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
65
|
+
setTimeout(
|
|
66
|
+
() => reject(
|
|
67
|
+
new TransactionFailedError(
|
|
68
|
+
"evm",
|
|
69
|
+
`UserOperation confirmation timed out after ${USER_OP_TIMEOUT_MS / 1e3}s. Hash: ${userOpHash}`
|
|
70
|
+
)
|
|
71
|
+
),
|
|
72
|
+
USER_OP_TIMEOUT_MS
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
const receipt = await Promise.race([response.included(), timeoutPromise]);
|
|
76
|
+
if (!receipt.success) {
|
|
77
|
+
throw new TransactionFailedError(
|
|
78
|
+
"evm",
|
|
79
|
+
`UserOperation reverted. Tx: ${receipt.receipt.transactionHash}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return receipt.receipt.transactionHash;
|
|
83
|
+
}
|
|
84
|
+
function handleTransferError(error, chain) {
|
|
85
|
+
if (!(error instanceof Error)) {
|
|
86
|
+
return new TransactionFailedError(chain, "Unknown error");
|
|
87
|
+
}
|
|
88
|
+
const msg = error.message;
|
|
89
|
+
if (msg.includes("callData reverts")) {
|
|
90
|
+
return new TransactionFailedError(
|
|
91
|
+
chain,
|
|
92
|
+
"Transaction reverted. Check your balance and try again."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (msg.includes("not enough funds")) {
|
|
96
|
+
return new InsufficientBalanceError(chain, "paymaster token");
|
|
97
|
+
}
|
|
98
|
+
if (msg.includes("token balance lower than the required") || msg.includes("token allowance lower than the required")) {
|
|
99
|
+
return new InsufficientBalanceError(chain, "USDT (insufficient for transfer + gas)");
|
|
100
|
+
}
|
|
101
|
+
if (msg.includes("Insufficient") || error instanceof InsufficientBalanceError) {
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
if (msg.includes("nonce too low")) {
|
|
105
|
+
return new TransactionFailedError(chain, "Nonce conflict. Please try again.");
|
|
106
|
+
}
|
|
107
|
+
if (msg.includes("insufficient funds") || msg.includes("transfer amount exceeds balance")) {
|
|
108
|
+
return new InsufficientBalanceError(chain, "USDT");
|
|
109
|
+
}
|
|
110
|
+
if (msg.includes("transaction underpriced") || msg.includes("max fee per gas less than block base fee")) {
|
|
111
|
+
return new TransactionFailedError(chain, "Gas price too low. Please try again.");
|
|
112
|
+
}
|
|
113
|
+
if (msg.includes("out of gas") || msg.includes("intrinsic gas too low")) {
|
|
114
|
+
return new TransactionFailedError(chain, "Insufficient gas limit.");
|
|
115
|
+
}
|
|
116
|
+
if (msg.includes("execution reverted")) {
|
|
117
|
+
return new TransactionFailedError(chain, "Transaction reverted on-chain.");
|
|
118
|
+
}
|
|
119
|
+
if (msg.includes("504") || msg.includes("Gateway Time-out")) {
|
|
120
|
+
return new TransactionFailedError(chain, "Network timeout. Please try again.");
|
|
121
|
+
}
|
|
122
|
+
return new TransactionFailedError(chain, msg);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/passkey/credentialStore.ts
|
|
126
|
+
var STORAGE_KEY = "@gasfree-kit/passkey-credentials";
|
|
127
|
+
function serialize(cred) {
|
|
128
|
+
return {
|
|
129
|
+
id: cred.id,
|
|
130
|
+
publicKey: {
|
|
131
|
+
x: cred.publicKey.x.toString(),
|
|
132
|
+
y: cred.publicKey.y.toString()
|
|
133
|
+
},
|
|
134
|
+
signerAddress: cred.signerAddress,
|
|
135
|
+
safeAddress: cred.safeAddress
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function deserialize(raw) {
|
|
139
|
+
if (!raw || typeof raw !== "object") {
|
|
140
|
+
throw new Error("Invalid stored credential: not an object");
|
|
141
|
+
}
|
|
142
|
+
const obj = raw;
|
|
143
|
+
if (typeof obj.id !== "string" || !obj.id) {
|
|
144
|
+
throw new Error("Invalid stored credential: missing or invalid id");
|
|
145
|
+
}
|
|
146
|
+
if (typeof obj.signerAddress !== "string" || !obj.signerAddress) {
|
|
147
|
+
throw new Error("Invalid stored credential: missing or invalid signerAddress");
|
|
148
|
+
}
|
|
149
|
+
if (typeof obj.safeAddress !== "string" || !obj.safeAddress) {
|
|
150
|
+
throw new Error("Invalid stored credential: missing or invalid safeAddress");
|
|
151
|
+
}
|
|
152
|
+
const pk = obj.publicKey;
|
|
153
|
+
if (!pk || typeof pk !== "object" || typeof pk.x !== "string" || typeof pk.y !== "string") {
|
|
154
|
+
throw new Error("Invalid stored credential: missing or invalid publicKey");
|
|
155
|
+
}
|
|
156
|
+
let x;
|
|
157
|
+
let y;
|
|
158
|
+
try {
|
|
159
|
+
x = BigInt(pk.x);
|
|
160
|
+
y = BigInt(pk.y);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error("Invalid stored credential: publicKey coordinates are not valid integers");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
id: obj.id,
|
|
166
|
+
publicKey: { x, y },
|
|
167
|
+
signerAddress: obj.signerAddress,
|
|
168
|
+
safeAddress: obj.safeAddress
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
var BrowserCredentialStore = class {
|
|
172
|
+
readAll() {
|
|
173
|
+
try {
|
|
174
|
+
const data = localStorage.getItem(STORAGE_KEY);
|
|
175
|
+
return data ? JSON.parse(data) : [];
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
writeAll(credentials) {
|
|
181
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(credentials));
|
|
182
|
+
}
|
|
183
|
+
async save(credential) {
|
|
184
|
+
const all = this.readAll();
|
|
185
|
+
const idx = all.findIndex(
|
|
186
|
+
(c) => c.safeAddress.toLowerCase() === credential.safeAddress.toLowerCase()
|
|
187
|
+
);
|
|
188
|
+
const serialized = serialize(credential);
|
|
189
|
+
if (idx >= 0) {
|
|
190
|
+
all[idx] = serialized;
|
|
191
|
+
} else {
|
|
192
|
+
all.push(serialized);
|
|
193
|
+
}
|
|
194
|
+
this.writeAll(all);
|
|
195
|
+
}
|
|
196
|
+
async load(safeAddress) {
|
|
197
|
+
const all = this.readAll();
|
|
198
|
+
const found = all.find((c) => c.safeAddress.toLowerCase() === safeAddress.toLowerCase());
|
|
199
|
+
return found ? deserialize(found) : null;
|
|
200
|
+
}
|
|
201
|
+
async remove(safeAddress) {
|
|
202
|
+
const all = this.readAll();
|
|
203
|
+
const filtered = all.filter((c) => c.safeAddress.toLowerCase() !== safeAddress.toLowerCase());
|
|
204
|
+
this.writeAll(filtered);
|
|
205
|
+
}
|
|
206
|
+
async listAll() {
|
|
207
|
+
return this.readAll().map(deserialize);
|
|
208
|
+
}
|
|
40
209
|
};
|
|
210
|
+
var NodeCredentialStore = class {
|
|
211
|
+
filePath = null;
|
|
212
|
+
async getFilePath() {
|
|
213
|
+
if (this.filePath) return this.filePath;
|
|
214
|
+
const os = await import("os");
|
|
215
|
+
const path = await import("path");
|
|
216
|
+
this.filePath = path.join(os.homedir(), ".gasfree-kit", "passkey-credentials.json");
|
|
217
|
+
return this.filePath;
|
|
218
|
+
}
|
|
219
|
+
async ensureDir(filePath) {
|
|
220
|
+
const path = await import("path");
|
|
221
|
+
const fs = await import("fs/promises");
|
|
222
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
|
|
223
|
+
}
|
|
224
|
+
async readAll() {
|
|
225
|
+
try {
|
|
226
|
+
const fs = await import("fs/promises");
|
|
227
|
+
const filePath = await this.getFilePath();
|
|
228
|
+
const data = await fs.readFile(filePath, "utf-8");
|
|
229
|
+
const parsed = JSON.parse(data);
|
|
230
|
+
if (!Array.isArray(parsed)) return [];
|
|
231
|
+
return parsed;
|
|
232
|
+
} catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async writeAll(credentials) {
|
|
237
|
+
const fs = await import("fs/promises");
|
|
238
|
+
const filePath = await this.getFilePath();
|
|
239
|
+
await this.ensureDir(filePath);
|
|
240
|
+
const tmpPath = filePath + ".tmp." + Date.now();
|
|
241
|
+
try {
|
|
242
|
+
await fs.writeFile(tmpPath, JSON.stringify(credentials, null, 2), {
|
|
243
|
+
encoding: "utf-8",
|
|
244
|
+
mode: 384
|
|
245
|
+
});
|
|
246
|
+
await fs.rename(tmpPath, filePath);
|
|
247
|
+
await fs.chmod(filePath, 384);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
try {
|
|
250
|
+
await fs.unlink(tmpPath);
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async save(credential) {
|
|
257
|
+
const all = await this.readAll();
|
|
258
|
+
const idx = all.findIndex(
|
|
259
|
+
(c) => c.safeAddress.toLowerCase() === credential.safeAddress.toLowerCase()
|
|
260
|
+
);
|
|
261
|
+
const serialized = serialize(credential);
|
|
262
|
+
if (idx >= 0) {
|
|
263
|
+
all[idx] = serialized;
|
|
264
|
+
} else {
|
|
265
|
+
all.push(serialized);
|
|
266
|
+
}
|
|
267
|
+
await this.writeAll(all);
|
|
268
|
+
}
|
|
269
|
+
async load(safeAddress) {
|
|
270
|
+
const all = await this.readAll();
|
|
271
|
+
const found = all.find((c) => c.safeAddress.toLowerCase() === safeAddress.toLowerCase());
|
|
272
|
+
return found ? deserialize(found) : null;
|
|
273
|
+
}
|
|
274
|
+
async remove(safeAddress) {
|
|
275
|
+
const all = await this.readAll();
|
|
276
|
+
const filtered = all.filter((c) => c.safeAddress.toLowerCase() !== safeAddress.toLowerCase());
|
|
277
|
+
await this.writeAll(filtered);
|
|
278
|
+
}
|
|
279
|
+
async listAll() {
|
|
280
|
+
return (await this.readAll()).map(deserialize);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function createCredentialStore() {
|
|
284
|
+
const isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined" && typeof navigator !== "undefined" && typeof navigator.credentials !== "undefined";
|
|
285
|
+
if (isBrowser) {
|
|
286
|
+
return new BrowserCredentialStore();
|
|
287
|
+
}
|
|
288
|
+
return new NodeCredentialStore();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/passkey/rpcUtils.ts
|
|
292
|
+
function sanitizeRpcUrl(rpcUrl) {
|
|
293
|
+
try {
|
|
294
|
+
const url = new URL(rpcUrl);
|
|
295
|
+
return url.origin;
|
|
296
|
+
} catch {
|
|
297
|
+
return "(invalid URL)";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function jsonRpcCall(rpcUrl, request) {
|
|
301
|
+
if (!rpcUrl) {
|
|
302
|
+
throw new Error("RPC URL is not configured");
|
|
303
|
+
}
|
|
304
|
+
const safeUrl = sanitizeRpcUrl(rpcUrl);
|
|
305
|
+
let response;
|
|
306
|
+
try {
|
|
307
|
+
response = await fetch(rpcUrl, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "Content-Type": "application/json" },
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
jsonrpc: "2.0",
|
|
312
|
+
id: 1,
|
|
313
|
+
method: request.method,
|
|
314
|
+
params: request.params
|
|
315
|
+
})
|
|
316
|
+
});
|
|
317
|
+
} catch (err) {
|
|
318
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
319
|
+
throw new Error(`RPC network error (${safeUrl}): ${msg}`);
|
|
320
|
+
}
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
throw new Error(`RPC HTTP ${response.status}: ${response.statusText} (${safeUrl})`);
|
|
323
|
+
}
|
|
324
|
+
let json;
|
|
325
|
+
try {
|
|
326
|
+
json = await response.json();
|
|
327
|
+
} catch {
|
|
328
|
+
throw new Error(`RPC returned non-JSON response (${safeUrl})`);
|
|
329
|
+
}
|
|
330
|
+
if (json.error) {
|
|
331
|
+
const code = json.error.code ? ` (${json.error.code})` : "";
|
|
332
|
+
throw new Error(`RPC error${code}: ${json.error.message ?? "unknown error"}`);
|
|
333
|
+
}
|
|
334
|
+
if (json.result === void 0 || json.result === null) {
|
|
335
|
+
throw new Error(`RPC returned null result for ${request.method}`);
|
|
336
|
+
}
|
|
337
|
+
return json.result;
|
|
338
|
+
}
|
|
339
|
+
async function isContractDeployed(rpcUrl, address) {
|
|
340
|
+
const code = await jsonRpcCall(rpcUrl, {
|
|
341
|
+
method: "eth_getCode",
|
|
342
|
+
params: [address, "latest"]
|
|
343
|
+
});
|
|
344
|
+
return code !== "0x" && code !== "0x0";
|
|
345
|
+
}
|
|
346
|
+
async function getSafeOwners(rpcUrl, safeAddress) {
|
|
347
|
+
const GET_OWNERS_SELECTOR = "0xa0e67e2b";
|
|
348
|
+
const result = await jsonRpcCall(rpcUrl, {
|
|
349
|
+
method: "eth_call",
|
|
350
|
+
params: [{ to: safeAddress, data: GET_OWNERS_SELECTOR }, "latest"]
|
|
351
|
+
});
|
|
352
|
+
const hex = result.startsWith("0x") ? result.slice(2) : result;
|
|
353
|
+
if (hex.length < 128) {
|
|
354
|
+
throw new Error(`Invalid getOwners response from ${safeAddress}`);
|
|
355
|
+
}
|
|
356
|
+
const arrayLength = parseInt(hex.slice(64, 128), 16);
|
|
357
|
+
const owners = [];
|
|
358
|
+
for (let i = 0; i < arrayLength; i++) {
|
|
359
|
+
const start = 128 + i * 64;
|
|
360
|
+
const ownerHex = hex.slice(start + 24, start + 64);
|
|
361
|
+
owners.push("0x" + ownerHex);
|
|
362
|
+
}
|
|
363
|
+
return owners;
|
|
364
|
+
}
|
|
365
|
+
async function isSafeOwner(rpcUrl, safeAddress, ownerAddress) {
|
|
366
|
+
const owners = await getSafeOwners(rpcUrl, safeAddress);
|
|
367
|
+
return owners.some((o) => o.toLowerCase() === ownerAddress.toLowerCase());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/passkey/types.ts
|
|
371
|
+
var SAFE_WEBAUTHN_SINGLETON = "0x270d7e4a57e6322f336261f3eae2bade72e68d72";
|
|
372
|
+
var SAFE_WEBAUTHN_SIGNER_PROXY_CREATION_CODE = "61010060405234801561001157600080fd5b506040516101ee3803806101ee83398101604081905261003091610058565b6001600160a01b0390931660805260a09190915260c0526001600160b01b031660e0526100bc565b6000806000806080858703121561006e57600080fd5b84516001600160a01b038116811461008557600080fd5b60208601516040870151606088015192965090945092506001600160b01b03811681146100b157600080fd5b939692955090935050565b60805160a05160c05160e05160ff6100ef60003960006008015260006031015260006059015260006080015260ff6000f3fe608060408190527f00000000000000000000000000000000000000000000000000000000000000003660b681018290527f000000000000000000000000000000000000000000000000000000000000000060a082018190527f00000000000000000000000000000000000000000000000000000000000000008285018190527f00000000000000000000000000000000000000000000000000000000000000009490939192600082376000806056360183885af490503d6000803e8060c3573d6000fd5b503d6000f3fea2646970667358221220ddd9bb059ba7a6497d560ca97aadf4dbf0476f578378554a50d41c6bb654beae64736f6c63430008180033";
|
|
373
|
+
var SAFE_WEBAUTHN_SIGNER_FACTORY = {
|
|
374
|
+
ethereum: "0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf",
|
|
375
|
+
base: "0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf",
|
|
376
|
+
arbitrum: "0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf",
|
|
377
|
+
optimism: "0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf",
|
|
378
|
+
polygon: "0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf",
|
|
379
|
+
celo: "0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf"
|
|
380
|
+
};
|
|
381
|
+
var FCL_P256_VERIFIER = {
|
|
382
|
+
ethereum: "0xc2b78104907F722DABAc4C69f826a522B2754De4",
|
|
383
|
+
base: "0xc2b78104907F722DABAc4C69f826a522B2754De4",
|
|
384
|
+
arbitrum: "0xc2b78104907F722DABAc4C69f826a522B2754De4",
|
|
385
|
+
optimism: "0xc2b78104907F722DABAc4C69f826a522B2754De4",
|
|
386
|
+
polygon: "0xc2b78104907F722DABAc4C69f826a522B2754De4",
|
|
387
|
+
celo: "0xc2b78104907F722DABAc4C69f826a522B2754De4"
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/passkey/linkPasskey.ts
|
|
391
|
+
var P256_FIELD_PRIME = BigInt(
|
|
392
|
+
"0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"
|
|
393
|
+
);
|
|
394
|
+
function validateAndPadAddress(addr) {
|
|
395
|
+
const hex = addr.startsWith("0x") ? addr.slice(2) : addr;
|
|
396
|
+
if (!/^[0-9a-fA-F]{40}$/.test(hex)) {
|
|
397
|
+
throw new Error(`Invalid EVM address for ABI encoding: ${addr}`);
|
|
398
|
+
}
|
|
399
|
+
return hex.toLowerCase().padStart(64, "0");
|
|
400
|
+
}
|
|
401
|
+
function validateUint256(value, name) {
|
|
402
|
+
if (value < 0n) {
|
|
403
|
+
throw new Error(`Invalid ${name}: ${value}. Must be non-negative.`);
|
|
404
|
+
}
|
|
405
|
+
const hex = value.toString(16);
|
|
406
|
+
if (hex.length > 64) {
|
|
407
|
+
throw new Error(`Invalid ${name}: exceeds uint256 max.`);
|
|
408
|
+
}
|
|
409
|
+
return hex.padStart(64, "0");
|
|
410
|
+
}
|
|
411
|
+
function encodeAddOwnerWithThreshold(owner, threshold) {
|
|
412
|
+
const selector = "0x0d582f13";
|
|
413
|
+
return `${selector}${validateAndPadAddress(owner)}${validateUint256(threshold, "threshold")}`;
|
|
414
|
+
}
|
|
415
|
+
function encodeRemoveOwner(prevOwner, owner, threshold) {
|
|
416
|
+
const selector = "0xf8dc5dd9";
|
|
417
|
+
return `${selector}${validateAndPadAddress(prevOwner)}${validateAndPadAddress(owner)}${validateUint256(threshold, "threshold")}`;
|
|
418
|
+
}
|
|
419
|
+
function encodeCreateSigner(x, y, verifier) {
|
|
420
|
+
const selector = "0xa8a65a78";
|
|
421
|
+
return `${selector}${validateUint256(x, "x")}${validateUint256(y, "y")}${validateAndPadAddress(verifier)}`;
|
|
422
|
+
}
|
|
423
|
+
function extractPublicKeyFromAttestation(credential) {
|
|
424
|
+
const response = credential.response;
|
|
425
|
+
const publicKey = response.getPublicKey();
|
|
426
|
+
if (!publicKey) {
|
|
427
|
+
throw new Error("Failed to extract public key from WebAuthn attestation");
|
|
428
|
+
}
|
|
429
|
+
const keyBytes = new Uint8Array(publicKey);
|
|
430
|
+
let offset = -1;
|
|
431
|
+
for (let i = 0; i <= keyBytes.length - 65; i++) {
|
|
432
|
+
if (keyBytes[i] === 4) {
|
|
433
|
+
offset = i + 1;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (offset === -1 || offset + 64 > keyBytes.length) {
|
|
438
|
+
throw new Error("Could not find P-256 public key in attestation data");
|
|
439
|
+
}
|
|
440
|
+
const xBytes = keyBytes.slice(offset, offset + 32);
|
|
441
|
+
const yBytes = keyBytes.slice(offset + 32, offset + 64);
|
|
442
|
+
const x = BigInt(
|
|
443
|
+
"0x" + Array.from(xBytes).map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
444
|
+
);
|
|
445
|
+
const y = BigInt(
|
|
446
|
+
"0x" + Array.from(yBytes).map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
447
|
+
);
|
|
448
|
+
if (x === 0n && y === 0n) {
|
|
449
|
+
throw new Error("Public key is point at infinity (invalid)");
|
|
450
|
+
}
|
|
451
|
+
if (x >= P256_FIELD_PRIME || y >= P256_FIELD_PRIME) {
|
|
452
|
+
throw new Error("Public key coordinates exceed P-256 field prime");
|
|
453
|
+
}
|
|
454
|
+
return { x, y };
|
|
455
|
+
}
|
|
456
|
+
function arrayBufferToBase64Url(buffer) {
|
|
457
|
+
const bytes = new Uint8Array(buffer);
|
|
458
|
+
let binary = "";
|
|
459
|
+
for (const byte of bytes) {
|
|
460
|
+
binary += String.fromCharCode(byte);
|
|
461
|
+
}
|
|
462
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
463
|
+
}
|
|
464
|
+
function deriveSignerAddress(factoryAddress, x, y, verifierAddress) {
|
|
465
|
+
const args = BigInt(SAFE_WEBAUTHN_SINGLETON).toString(16).padStart(64, "0") + x.toString(16).padStart(64, "0") + y.toString(16).padStart(64, "0") + BigInt(verifierAddress).toString(16).padStart(64, "0");
|
|
466
|
+
const initCode = SAFE_WEBAUTHN_SIGNER_PROXY_CREATION_CODE + args;
|
|
467
|
+
const codeHash = bytesToHex(keccak_256(hexToBytes(initCode)));
|
|
468
|
+
const salt = "0".repeat(64);
|
|
469
|
+
const create2Input = "ff" + factoryAddress.slice(2).toLowerCase() + salt + codeHash;
|
|
470
|
+
const create2Hash = bytesToHex(keccak_256(hexToBytes(create2Input)));
|
|
471
|
+
return "0x" + create2Hash.slice(24);
|
|
472
|
+
}
|
|
473
|
+
async function linkPasskeyToSafe(seedPhrase, config, passkeyOptions = {}, accountIndex = 0) {
|
|
474
|
+
const networkConfig = getErc4337ConfigForChain(config);
|
|
475
|
+
const {
|
|
476
|
+
wallet,
|
|
477
|
+
account,
|
|
478
|
+
address: safeAddress
|
|
479
|
+
} = await setupErc4337Wallet(seedPhrase, config, accountIndex);
|
|
480
|
+
try {
|
|
481
|
+
const deployed = await isContractDeployed(networkConfig.provider, safeAddress);
|
|
482
|
+
if (!deployed) {
|
|
483
|
+
throw new TransactionFailedError2(
|
|
484
|
+
config.chain,
|
|
485
|
+
`Safe account at ${safeAddress} is not yet deployed on-chain. Send a transaction first to deploy it.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const {
|
|
489
|
+
rpId = typeof window !== "undefined" ? window.location.hostname : "localhost",
|
|
490
|
+
rpName = "Gasfree Kit",
|
|
491
|
+
userName = "Wallet User",
|
|
492
|
+
storage = "not_persist"
|
|
493
|
+
} = passkeyOptions;
|
|
494
|
+
const signerFactoryAddress = SAFE_WEBAUTHN_SIGNER_FACTORY[config.chain];
|
|
495
|
+
const p256VerifierAddress = FCL_P256_VERIFIER[config.chain];
|
|
496
|
+
if (!signerFactoryAddress || !p256VerifierAddress) {
|
|
497
|
+
throw new TransactionFailedError2(
|
|
498
|
+
config.chain,
|
|
499
|
+
`Passkey contracts not available on ${config.chain}`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const credentialPromise = navigator.credentials.create({
|
|
503
|
+
publicKey: {
|
|
504
|
+
rp: { id: rpId, name: rpName },
|
|
505
|
+
user: {
|
|
506
|
+
// Use an opaque random handle so the WebAuthn user record does not expose the Safe address.
|
|
507
|
+
id: crypto.getRandomValues(new Uint8Array(32)).buffer,
|
|
508
|
+
name: userName,
|
|
509
|
+
displayName: userName
|
|
510
|
+
},
|
|
511
|
+
challenge: Uint8Array.from(crypto.getRandomValues(new Uint8Array(32))).buffer,
|
|
512
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
|
513
|
+
// ES256 (P-256)
|
|
514
|
+
authenticatorSelection: {
|
|
515
|
+
authenticatorAttachment: "platform",
|
|
516
|
+
residentKey: "required",
|
|
517
|
+
userVerification: "required"
|
|
518
|
+
},
|
|
519
|
+
timeout: 6e4
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
523
|
+
setTimeout(
|
|
524
|
+
() => reject(new TransactionFailedError2(config.chain, "Passkey registration timed out")),
|
|
525
|
+
65e3
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
const credential = await Promise.race([
|
|
529
|
+
credentialPromise,
|
|
530
|
+
timeoutPromise
|
|
531
|
+
]);
|
|
532
|
+
if (!credential) {
|
|
533
|
+
throw new TransactionFailedError2(config.chain, "Passkey registration was cancelled");
|
|
534
|
+
}
|
|
535
|
+
const publicKey = extractPublicKeyFromAttestation(credential);
|
|
536
|
+
const credentialId = arrayBufferToBase64Url(credential.rawId);
|
|
537
|
+
const signerAddress = deriveSignerAddress(
|
|
538
|
+
signerFactoryAddress,
|
|
539
|
+
publicKey.x,
|
|
540
|
+
publicKey.y,
|
|
541
|
+
p256VerifierAddress
|
|
542
|
+
);
|
|
543
|
+
const alreadyOwner = await isSafeOwner(networkConfig.provider, safeAddress, signerAddress);
|
|
544
|
+
if (alreadyOwner) {
|
|
545
|
+
throw new TransactionFailedError2(
|
|
546
|
+
config.chain,
|
|
547
|
+
`Passkey signer ${signerAddress} is already a Safe owner. Cannot link the same passkey twice.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
const createSignerCalldata = encodeCreateSigner(publicKey.x, publicKey.y, p256VerifierAddress);
|
|
551
|
+
const deployResult = await account.sendTransaction([
|
|
552
|
+
{
|
|
553
|
+
to: signerFactoryAddress,
|
|
554
|
+
value: 0,
|
|
555
|
+
data: createSignerCalldata
|
|
556
|
+
}
|
|
557
|
+
]);
|
|
558
|
+
await waitForUserOpConfirmation(
|
|
559
|
+
deployResult.hash,
|
|
560
|
+
networkConfig.bundlerUrl,
|
|
561
|
+
networkConfig.entryPointAddress
|
|
562
|
+
);
|
|
563
|
+
if (!await isContractDeployed(networkConfig.provider, signerAddress)) {
|
|
564
|
+
throw new TransactionFailedError2(
|
|
565
|
+
config.chain,
|
|
566
|
+
`Passkey signer deployment could not be verified at ${signerAddress}`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const addOwnerCalldata = encodeAddOwnerWithThreshold(signerAddress, 1n);
|
|
570
|
+
const addOwnerResult = await account.sendTransaction([
|
|
571
|
+
{
|
|
572
|
+
to: safeAddress,
|
|
573
|
+
value: 0,
|
|
574
|
+
data: addOwnerCalldata
|
|
575
|
+
}
|
|
576
|
+
]);
|
|
577
|
+
const transactionHash = await waitForUserOpConfirmation(
|
|
578
|
+
addOwnerResult.hash,
|
|
579
|
+
networkConfig.bundlerUrl,
|
|
580
|
+
networkConfig.entryPointAddress
|
|
581
|
+
);
|
|
582
|
+
const passkeyCredential = {
|
|
583
|
+
id: credentialId,
|
|
584
|
+
publicKey,
|
|
585
|
+
signerAddress,
|
|
586
|
+
safeAddress
|
|
587
|
+
};
|
|
588
|
+
let persisted = false;
|
|
589
|
+
if (storage === "persist") {
|
|
590
|
+
const store = createCredentialStore();
|
|
591
|
+
await store.save(passkeyCredential);
|
|
592
|
+
persisted = true;
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
credential: passkeyCredential,
|
|
596
|
+
safeAddress,
|
|
597
|
+
transactionHash,
|
|
598
|
+
persisted
|
|
599
|
+
};
|
|
600
|
+
} finally {
|
|
601
|
+
account.dispose();
|
|
602
|
+
wallet.dispose();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function isPasskeyLinked(seedPhrase, config, accountIndex = 0) {
|
|
606
|
+
const store = createCredentialStore();
|
|
607
|
+
const {
|
|
608
|
+
wallet,
|
|
609
|
+
account,
|
|
610
|
+
address: safeAddress
|
|
611
|
+
} = await setupErc4337Wallet(seedPhrase, config, accountIndex);
|
|
612
|
+
try {
|
|
613
|
+
const stored = await store.load(safeAddress);
|
|
614
|
+
if (stored) {
|
|
615
|
+
const networkConfig = getErc4337ConfigForChain(config);
|
|
616
|
+
const onChainOwner = await isSafeOwner(
|
|
617
|
+
networkConfig.provider,
|
|
618
|
+
safeAddress,
|
|
619
|
+
stored.signerAddress
|
|
620
|
+
);
|
|
621
|
+
if (onChainOwner) {
|
|
622
|
+
return { linked: true, signerAddress: stored.signerAddress };
|
|
623
|
+
}
|
|
624
|
+
await store.remove(safeAddress);
|
|
625
|
+
}
|
|
626
|
+
return { linked: false };
|
|
627
|
+
} finally {
|
|
628
|
+
account.dispose();
|
|
629
|
+
wallet.dispose();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function unlinkPasskeyFromSafe(seedPhrase, config, credential, accountIndex = 0) {
|
|
633
|
+
const networkConfig = getErc4337ConfigForChain(config);
|
|
634
|
+
const {
|
|
635
|
+
wallet,
|
|
636
|
+
account,
|
|
637
|
+
address: safeAddress
|
|
638
|
+
} = await setupErc4337Wallet(seedPhrase, config, accountIndex);
|
|
639
|
+
try {
|
|
640
|
+
if (credential.safeAddress.toLowerCase() !== safeAddress.toLowerCase()) {
|
|
641
|
+
throw new TransactionFailedError2(
|
|
642
|
+
config.chain,
|
|
643
|
+
`Credential belongs to Safe ${credential.safeAddress}, not ${safeAddress}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const owners = await getSafeOwners(networkConfig.provider, safeAddress);
|
|
647
|
+
const signerLower = credential.signerAddress.toLowerCase();
|
|
648
|
+
const ownerIndex = owners.findIndex((o) => o.toLowerCase() === signerLower);
|
|
649
|
+
if (ownerIndex === -1) {
|
|
650
|
+
throw new TransactionFailedError2(
|
|
651
|
+
config.chain,
|
|
652
|
+
`Passkey signer ${credential.signerAddress} is not a Safe owner`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const SENTINEL = "0x0000000000000000000000000000000000000001";
|
|
656
|
+
const prevOwner = ownerIndex === 0 ? SENTINEL : owners[ownerIndex - 1];
|
|
657
|
+
const removeOwnerCalldata = encodeRemoveOwner(prevOwner, credential.signerAddress, 1n);
|
|
658
|
+
const result = await account.sendTransaction([
|
|
659
|
+
{
|
|
660
|
+
to: safeAddress,
|
|
661
|
+
value: 0,
|
|
662
|
+
data: removeOwnerCalldata
|
|
663
|
+
}
|
|
664
|
+
]);
|
|
665
|
+
const transactionHash = await waitForUserOpConfirmation(
|
|
666
|
+
result.hash,
|
|
667
|
+
networkConfig.bundlerUrl,
|
|
668
|
+
networkConfig.entryPointAddress
|
|
669
|
+
);
|
|
670
|
+
const store = createCredentialStore();
|
|
671
|
+
await store.remove(safeAddress);
|
|
672
|
+
return { transactionHash };
|
|
673
|
+
} finally {
|
|
674
|
+
account.dispose();
|
|
675
|
+
wallet.dispose();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
41
678
|
|
|
42
679
|
// src/evmERC4337.ts
|
|
43
|
-
async function setupErc4337Wallet(seedPhrase, config, accountIndex = 0) {
|
|
680
|
+
async function setupErc4337Wallet(seedPhrase, config, accountIndex = 0, passkeyOptions) {
|
|
44
681
|
const networkConfig = getErc4337ConfigForChain(config);
|
|
45
682
|
const WalletManagerEvmErc4337Module = await import("@tetherto/wdk-wallet-evm-erc-4337");
|
|
46
683
|
const WalletManagerEvmErc4337 = WalletManagerEvmErc4337Module.default;
|
|
@@ -69,14 +706,17 @@ async function setupErc4337Wallet(seedPhrase, config, accountIndex = 0) {
|
|
|
69
706
|
);
|
|
70
707
|
const account = await wallet.getAccount(accountIndex);
|
|
71
708
|
const address = await account.getAddress();
|
|
72
|
-
|
|
709
|
+
let passkey;
|
|
710
|
+
if (passkeyOptions) {
|
|
711
|
+
passkey = await linkPasskeyToSafe(seedPhrase, config, passkeyOptions, accountIndex);
|
|
712
|
+
}
|
|
713
|
+
return { wallet, account, address, passkey };
|
|
73
714
|
}
|
|
74
715
|
|
|
75
716
|
// src/evmERC4337TetherTransfer.ts
|
|
76
|
-
import { Bundler, SendUseroperationResponse } from "abstractionkit";
|
|
77
717
|
import {
|
|
78
|
-
InsufficientBalanceError,
|
|
79
|
-
TransactionFailedError,
|
|
718
|
+
InsufficientBalanceError as InsufficientBalanceError2,
|
|
719
|
+
TransactionFailedError as TransactionFailedError3,
|
|
80
720
|
validateEvmAddress
|
|
81
721
|
} from "@gasfree-kit/core";
|
|
82
722
|
|
|
@@ -128,36 +768,7 @@ function feeToUsdt(feeInWei, nativeTokenPriceUsdt) {
|
|
|
128
768
|
}
|
|
129
769
|
|
|
130
770
|
// src/evmERC4337TetherTransfer.ts
|
|
131
|
-
var
|
|
132
|
-
/** Default timeout for UserOp confirmation: 120 seconds. */
|
|
133
|
-
static USER_OP_TIMEOUT_MS = 12e4;
|
|
134
|
-
/**
|
|
135
|
-
* Wait for a UserOperation to be included on-chain via the Candide bundler.
|
|
136
|
-
* Times out after USER_OP_TIMEOUT_MS to prevent indefinite hangs.
|
|
137
|
-
*/
|
|
138
|
-
static async waitForUserOpConfirmation(userOpHash, bundlerUrl, entryPointAddress) {
|
|
139
|
-
const bundler = new Bundler(bundlerUrl);
|
|
140
|
-
const response = new SendUseroperationResponse(userOpHash, bundler, entryPointAddress);
|
|
141
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
142
|
-
setTimeout(
|
|
143
|
-
() => reject(
|
|
144
|
-
new TransactionFailedError(
|
|
145
|
-
"evm",
|
|
146
|
-
`UserOperation confirmation timed out after ${this.USER_OP_TIMEOUT_MS / 1e3}s. Hash: ${userOpHash}`
|
|
147
|
-
)
|
|
148
|
-
),
|
|
149
|
-
this.USER_OP_TIMEOUT_MS
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
const receipt = await Promise.race([response.included(), timeoutPromise]);
|
|
153
|
-
if (!receipt.success) {
|
|
154
|
-
throw new TransactionFailedError(
|
|
155
|
-
"evm",
|
|
156
|
-
`UserOperation reverted. Tx: ${receipt.receipt.transactionHash}`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
return receipt.receipt.transactionHash;
|
|
160
|
-
}
|
|
771
|
+
var EvmTransfer = class {
|
|
161
772
|
/**
|
|
162
773
|
* Get transaction fee estimate for a USDT transfer.
|
|
163
774
|
*
|
|
@@ -250,7 +861,7 @@ var TetherEVMERC4337Transfer = class {
|
|
|
250
861
|
} = await setupErc4337Wallet(seedPhrase, config, accountIndex);
|
|
251
862
|
try {
|
|
252
863
|
if (recipientAddress.toLowerCase() === senderAddress.toLowerCase()) {
|
|
253
|
-
throw new
|
|
864
|
+
throw new TransactionFailedError3(config.chain, "Cannot transfer to your own address");
|
|
254
865
|
}
|
|
255
866
|
const amountInBaseUnits = toUsdtBaseUnitsEvm(amount);
|
|
256
867
|
const balance = await account.getTokenBalance(tokenAddress);
|
|
@@ -269,20 +880,20 @@ var TetherEVMERC4337Transfer = class {
|
|
|
269
880
|
}
|
|
270
881
|
const totalRequired = amountInBaseUnits + gasFeeBaseUnits;
|
|
271
882
|
if (BigInt(balance) < totalRequired) {
|
|
272
|
-
throw new
|
|
883
|
+
throw new InsufficientBalanceError2(
|
|
273
884
|
config.chain,
|
|
274
885
|
`USDT (need ${formatTokenBalance(totalRequired, 6)} but have ${formatTokenBalance(balance, 6)} \u2014 includes ${formatTokenBalance(gasFeeBaseUnits, 6)} gas fee)`
|
|
275
886
|
);
|
|
276
887
|
}
|
|
277
888
|
} else if (BigInt(balance) < amountInBaseUnits) {
|
|
278
|
-
throw new
|
|
889
|
+
throw new InsufficientBalanceError2(config.chain, "USDT");
|
|
279
890
|
}
|
|
280
891
|
const transferResult = await account.transfer({
|
|
281
892
|
token: tokenAddress,
|
|
282
893
|
recipient: recipientAddress,
|
|
283
894
|
amount: amountInBaseUnits
|
|
284
895
|
});
|
|
285
|
-
const transactionHash = await
|
|
896
|
+
const transactionHash = await waitForUserOpConfirmation(
|
|
286
897
|
transferResult.hash,
|
|
287
898
|
networkConfig.bundlerUrl,
|
|
288
899
|
networkConfig.entryPointAddress
|
|
@@ -294,7 +905,7 @@ var TetherEVMERC4337Transfer = class {
|
|
|
294
905
|
amount
|
|
295
906
|
};
|
|
296
907
|
} catch (error) {
|
|
297
|
-
throw
|
|
908
|
+
throw handleTransferError(error, config.chain);
|
|
298
909
|
} finally {
|
|
299
910
|
account.dispose();
|
|
300
911
|
wallet.dispose();
|
|
@@ -317,7 +928,7 @@ var TetherEVMERC4337Transfer = class {
|
|
|
317
928
|
try {
|
|
318
929
|
for (const r of recipients) {
|
|
319
930
|
if (r.address.toLowerCase() === senderAddress.toLowerCase()) {
|
|
320
|
-
throw new
|
|
931
|
+
throw new TransactionFailedError3(config.chain, "Cannot transfer to your own address");
|
|
321
932
|
}
|
|
322
933
|
}
|
|
323
934
|
const totalAmount = recipients.reduce((sum, r) => sum + toUsdtBaseUnitsEvm(r.amount), 0n);
|
|
@@ -326,13 +937,13 @@ var TetherEVMERC4337Transfer = class {
|
|
|
326
937
|
const gasFeeReserve = GAS_FEE_FALLBACKS[config.chain] ?? 150000n;
|
|
327
938
|
const totalRequired = totalAmount + gasFeeReserve;
|
|
328
939
|
if (BigInt(balance) < totalRequired) {
|
|
329
|
-
throw new
|
|
940
|
+
throw new InsufficientBalanceError2(
|
|
330
941
|
config.chain,
|
|
331
942
|
`USDT (need ${formatTokenBalance(totalRequired, 6)} but have ${formatTokenBalance(balance, 6)} \u2014 includes ${formatTokenBalance(gasFeeReserve, 6)} gas fee)`
|
|
332
943
|
);
|
|
333
944
|
}
|
|
334
945
|
} else if (BigInt(balance) < totalAmount) {
|
|
335
|
-
throw new
|
|
946
|
+
throw new InsufficientBalanceError2(config.chain, "USDT");
|
|
336
947
|
}
|
|
337
948
|
const batchTransactions = recipients.map(({ address, amount }) => {
|
|
338
949
|
const amountBase = toUsdtBaseUnitsEvm(amount);
|
|
@@ -343,7 +954,7 @@ var TetherEVMERC4337Transfer = class {
|
|
|
343
954
|
};
|
|
344
955
|
});
|
|
345
956
|
const transferResult = await account.sendTransaction(batchTransactions);
|
|
346
|
-
const transactionHash = await
|
|
957
|
+
const transactionHash = await waitForUserOpConfirmation(
|
|
347
958
|
transferResult.hash,
|
|
348
959
|
networkConfig.bundlerUrl,
|
|
349
960
|
networkConfig.entryPointAddress
|
|
@@ -355,64 +966,171 @@ var TetherEVMERC4337Transfer = class {
|
|
|
355
966
|
recipients
|
|
356
967
|
};
|
|
357
968
|
} catch (error) {
|
|
358
|
-
throw
|
|
969
|
+
throw handleTransferError(error, config.chain);
|
|
359
970
|
} finally {
|
|
360
971
|
account.dispose();
|
|
361
972
|
wallet.dispose();
|
|
362
973
|
}
|
|
363
974
|
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// src/passkey/passkeyTransfer.ts
|
|
978
|
+
import { TransactionFailedError as TransactionFailedError4, validateEvmAddress as validateEvmAddress2 } from "@gasfree-kit/core";
|
|
979
|
+
var ERC20_BALANCE_OF_SELECTOR = "0x70a08231";
|
|
980
|
+
async function loadSafe4337Pack() {
|
|
981
|
+
try {
|
|
982
|
+
const module = await import("@safe-global/relay-kit");
|
|
983
|
+
return module.Safe4337Pack;
|
|
984
|
+
} catch {
|
|
985
|
+
throw new Error(
|
|
986
|
+
"Passkey transfers require @safe-global/relay-kit. Install it: npm install @safe-global/relay-kit"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function createPasskeyPack(credential, config) {
|
|
991
|
+
const Safe4337Pack = await loadSafe4337Pack();
|
|
992
|
+
const networkConfig = getErc4337ConfigForChain(config);
|
|
993
|
+
const passkeySigner = {
|
|
994
|
+
rawId: credential.id,
|
|
995
|
+
coordinates: {
|
|
996
|
+
x: credential.publicKey.x,
|
|
997
|
+
y: credential.publicKey.y
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const paymasterOptions = networkConfig.isSponsored ? {
|
|
1001
|
+
isSponsored: true,
|
|
1002
|
+
paymasterUrl: networkConfig.paymasterUrl,
|
|
1003
|
+
...networkConfig.sponsorshipPolicyId ? { sponsorshipPolicyId: networkConfig.sponsorshipPolicyId } : {}
|
|
1004
|
+
} : {
|
|
1005
|
+
paymasterAddress: networkConfig.paymasterAddress,
|
|
1006
|
+
paymasterUrl: networkConfig.paymasterUrl
|
|
1007
|
+
};
|
|
1008
|
+
const pack = await Safe4337Pack.init({
|
|
1009
|
+
provider: networkConfig.provider,
|
|
1010
|
+
signer: passkeySigner,
|
|
1011
|
+
bundlerUrl: networkConfig.bundlerUrl,
|
|
1012
|
+
safeAddress: credential.safeAddress,
|
|
1013
|
+
paymasterOptions
|
|
1014
|
+
});
|
|
1015
|
+
return { pack, networkConfig };
|
|
1016
|
+
}
|
|
1017
|
+
var PasskeyTransfer = class {
|
|
364
1018
|
/**
|
|
365
|
-
*
|
|
1019
|
+
* Send a single token transfer signed by passkey.
|
|
1020
|
+
* Triggers biometric authentication via WebAuthn.
|
|
366
1021
|
*/
|
|
367
|
-
static
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (msg.includes("callData reverts")) {
|
|
373
|
-
return new TransactionFailedError(
|
|
374
|
-
chain,
|
|
375
|
-
"Transaction reverted. Check your balance and try again."
|
|
376
|
-
);
|
|
1022
|
+
static async sendToken(credential, config, tokenAddress, amount, recipientAddress) {
|
|
1023
|
+
validateEvmAddress2(tokenAddress, config.chain, "token address");
|
|
1024
|
+
validateEvmAddress2(recipientAddress, config.chain, "recipient address");
|
|
1025
|
+
if (recipientAddress.toLowerCase() === credential.safeAddress.toLowerCase()) {
|
|
1026
|
+
throw new TransactionFailedError4(config.chain, "Cannot transfer to your own address");
|
|
377
1027
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
1028
|
+
try {
|
|
1029
|
+
const { pack } = await createPasskeyPack(credential, config);
|
|
1030
|
+
const amountInBaseUnits = toUsdtBaseUnitsEvm(amount);
|
|
1031
|
+
const transferCalldata = encodeErc20Transfer(recipientAddress, amountInBaseUnits);
|
|
1032
|
+
const safeOperation = await pack.createTransaction({
|
|
1033
|
+
transactions: [
|
|
1034
|
+
{
|
|
1035
|
+
to: tokenAddress,
|
|
1036
|
+
data: transferCalldata,
|
|
1037
|
+
value: "0"
|
|
1038
|
+
}
|
|
1039
|
+
]
|
|
1040
|
+
});
|
|
1041
|
+
const signedOperation = await pack.signSafeOperation(safeOperation);
|
|
1042
|
+
const userOpHash = await pack.executeTransaction({
|
|
1043
|
+
executable: signedOperation
|
|
1044
|
+
});
|
|
1045
|
+
const receipt = await pack.getUserOperationReceipt(userOpHash);
|
|
1046
|
+
return {
|
|
1047
|
+
success: true,
|
|
1048
|
+
transactionHash: receipt.receipt.transactionHash,
|
|
1049
|
+
chain: config.chain,
|
|
1050
|
+
amount
|
|
1051
|
+
};
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
throw handleTransferError(error, config.chain);
|
|
398
1054
|
}
|
|
399
|
-
|
|
400
|
-
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Send a batch token transfer to multiple recipients, signed by passkey.
|
|
1058
|
+
*/
|
|
1059
|
+
static async sendBatchToken(credential, config, tokenAddress, recipients) {
|
|
1060
|
+
for (const r of recipients) {
|
|
1061
|
+
validateEvmAddress2(r.address, config.chain, "recipient address");
|
|
1062
|
+
if (r.address.toLowerCase() === credential.safeAddress.toLowerCase()) {
|
|
1063
|
+
throw new TransactionFailedError4(config.chain, "Cannot transfer to your own address");
|
|
1064
|
+
}
|
|
401
1065
|
}
|
|
402
|
-
|
|
403
|
-
|
|
1066
|
+
try {
|
|
1067
|
+
const { pack } = await createPasskeyPack(credential, config);
|
|
1068
|
+
const transactions = recipients.map(({ address, amount }) => {
|
|
1069
|
+
const amountBase = toUsdtBaseUnitsEvm(amount);
|
|
1070
|
+
return {
|
|
1071
|
+
to: tokenAddress,
|
|
1072
|
+
data: encodeErc20Transfer(address, amountBase),
|
|
1073
|
+
value: "0"
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
const safeOperation = await pack.createTransaction({ transactions });
|
|
1077
|
+
const signedOperation = await pack.signSafeOperation(safeOperation);
|
|
1078
|
+
const userOpHash = await pack.executeTransaction({ executable: signedOperation });
|
|
1079
|
+
const receipt = await pack.getUserOperationReceipt(userOpHash);
|
|
1080
|
+
return {
|
|
1081
|
+
success: true,
|
|
1082
|
+
transactionHash: receipt.receipt.transactionHash,
|
|
1083
|
+
chain: config.chain,
|
|
1084
|
+
recipients
|
|
1085
|
+
};
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
throw handleTransferError(error, config.chain);
|
|
404
1088
|
}
|
|
405
|
-
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Check token balance for a passkey-linked Safe account.
|
|
1092
|
+
* Does NOT require biometric — balance is publicly readable via RPC.
|
|
1093
|
+
*/
|
|
1094
|
+
static async checkTokenBalance(credential, config, tokenAddress) {
|
|
1095
|
+
validateEvmAddress2(tokenAddress, config.chain, "token address");
|
|
1096
|
+
validateEvmAddress2(credential.safeAddress, config.chain, "safe address");
|
|
1097
|
+
const networkConfig = getErc4337ConfigForChain(config);
|
|
1098
|
+
const addr = (credential.safeAddress.startsWith("0x") ? credential.safeAddress.slice(2) : credential.safeAddress).toLowerCase().padStart(64, "0");
|
|
1099
|
+
const balanceCalldata = `${ERC20_BALANCE_OF_SELECTOR}${addr}`;
|
|
1100
|
+
const result = await jsonRpcCall(networkConfig.provider, {
|
|
1101
|
+
method: "eth_call",
|
|
1102
|
+
params: [{ to: tokenAddress, data: balanceCalldata }, "latest"]
|
|
1103
|
+
});
|
|
1104
|
+
const balance = BigInt(result);
|
|
1105
|
+
const usdBalance = formatTokenBalance(balance, 6);
|
|
1106
|
+
return {
|
|
1107
|
+
message: `${config.chain.toUpperCase()} balances retrieved successfully`,
|
|
1108
|
+
success: true,
|
|
1109
|
+
data: {
|
|
1110
|
+
tokenBalance: balance.toString(),
|
|
1111
|
+
decimals: 6,
|
|
1112
|
+
usdBalance: String(usdBalance)
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
406
1115
|
}
|
|
407
1116
|
};
|
|
408
1117
|
export {
|
|
1118
|
+
EvmTransfer,
|
|
1119
|
+
FCL_P256_VERIFIER,
|
|
409
1120
|
GAS_FEE_ESTIMATES,
|
|
410
1121
|
GAS_FEE_FALLBACKS,
|
|
411
|
-
|
|
1122
|
+
PasskeyTransfer,
|
|
1123
|
+
SAFE_WEBAUTHN_SIGNER_FACTORY,
|
|
1124
|
+
createCredentialStore,
|
|
412
1125
|
encodeErc20Transfer,
|
|
413
1126
|
feeToUsdt,
|
|
414
1127
|
formatTokenBalance,
|
|
415
1128
|
getErc4337ConfigForChain,
|
|
1129
|
+
handleTransferError,
|
|
1130
|
+
isPasskeyLinked,
|
|
1131
|
+
linkPasskeyToSafe,
|
|
416
1132
|
setupErc4337Wallet,
|
|
417
|
-
toUsdtBaseUnitsEvm
|
|
1133
|
+
toUsdtBaseUnitsEvm,
|
|
1134
|
+
unlinkPasskeyFromSafe,
|
|
1135
|
+
waitForUserOpConfirmation
|
|
418
1136
|
};
|