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