@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.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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
285
|
-
(0,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1068
|
+
* Send a single token transfer signed by passkey.
|
|
1069
|
+
* Triggers biometric authentication via WebAuthn.
|
|
406
1070
|
*/
|
|
407
|
-
static
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|