@sequence0/sdk 1.0.2 → 1.1.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.
@@ -0,0 +1,810 @@
1
+ "use strict";
2
+ /**
3
+ * ERC-4337 Account Abstraction — Sequence0Account
4
+ *
5
+ * A smart account backed by FROST threshold signing. Enables gas-sponsored
6
+ * transactions, batch calls, and programmable transaction validation --
7
+ * all secured by the decentralized Sequence0 signing network.
8
+ *
9
+ * The account uses a Schnorr signature verifier on-chain: the FROST group
10
+ * public key is the account's "owner", and every UserOperation is signed
11
+ * by t-of-n agents producing a BIP-340 Schnorr signature.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { Sequence0Account } from '@sequence0/sdk';
16
+ *
17
+ * // Create a new AA wallet backed by FROST
18
+ * const account = await Sequence0Account.create({
19
+ * threshold: 16,
20
+ * committeeSize: 24,
21
+ * chain: 'ethereum',
22
+ * ownerPrivateKey: '0x...',
23
+ * });
24
+ *
25
+ * // Get the counterfactual address (before deployment)
26
+ * console.log('Address:', account.getAddress());
27
+ *
28
+ * // Send a transaction via UserOperation
29
+ * const txHash = await account.sendTransaction({
30
+ * to: '0xRecipient...',
31
+ * value: BigInt('1000000000000000000'), // 1 ETH
32
+ * });
33
+ *
34
+ * // Batch multiple calls
35
+ * const txHash2 = await account.sendBatchTransaction([
36
+ * { to: '0xTokenA', data: '0xa9059cbb...' },
37
+ * { to: '0xTokenB', data: '0xa9059cbb...' },
38
+ * ]);
39
+ * ```
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.Sequence0Account = void 0;
43
+ const client_1 = require("../core/client");
44
+ const errors_1 = require("../utils/errors");
45
+ const ethers_1 = require("ethers");
46
+ const types_1 = require("./types");
47
+ // ── Constants ──
48
+ /** Function selector: execute(address dest, uint256 value, bytes func) */
49
+ const EXECUTE_SELECTOR = '0xb61d27f6';
50
+ /** Function selector: executeBatch(address[] dest, uint256[] value, bytes[] func) */
51
+ const EXECUTE_BATCH_SELECTOR = '0x34fcd5be';
52
+ /** Function selector: createAccount(bytes groupPublicKey) */
53
+ const CREATE_ACCOUNT_SELECTOR_HASH = 'createAccount(bytes)';
54
+ /** Function selector: getNonce(address sender, uint192 key) on EntryPoint */
55
+ const GET_NONCE_SELECTOR = '0x35567e1a';
56
+ // ── Account Class ──
57
+ class Sequence0Account {
58
+ constructor(params) {
59
+ /** Whether the account has been deployed on-chain */
60
+ this.deployed = false;
61
+ this.accountAddress = params.accountAddress;
62
+ this.groupPublicKey = params.groupPublicKey;
63
+ this.walletId = params.walletId;
64
+ this.s0 = params.s0;
65
+ this.chain = params.chain;
66
+ this.chainId = params.chainId;
67
+ this.entryPoint = params.entryPoint;
68
+ this.factory = params.factory;
69
+ this.provider = params.provider;
70
+ this.bundlerUrl = params.bundlerUrl;
71
+ this.deployed = params.deployed;
72
+ }
73
+ // ────────────────────────────────────────────────
74
+ // Static Factory
75
+ // ────────────────────────────────────────────────
76
+ /**
77
+ * Create a new ERC-4337 smart account backed by FROST threshold signing.
78
+ *
79
+ * This performs a DKG ceremony to create a new FROST wallet, then
80
+ * derives the counterfactual smart account address using CREATE2.
81
+ * The account is not deployed on-chain until the first UserOperation
82
+ * is sent (initCode handles deployment).
83
+ *
84
+ * @param options - Account creation options
85
+ * @returns A new Sequence0Account instance
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const account = await Sequence0Account.create({
90
+ * threshold: 16,
91
+ * committeeSize: 24,
92
+ * chain: 'ethereum',
93
+ * ownerPrivateKey: '0x...',
94
+ * });
95
+ * ```
96
+ */
97
+ static async create(options) {
98
+ const network = options.network || 'mainnet';
99
+ const chain = options.chain || 'ethereum';
100
+ const chainId = types_1.CHAIN_IDS[chain];
101
+ if (!chainId) {
102
+ throw new errors_1.ChainError(`Unsupported chain for ERC-4337: '${chain}'. ` +
103
+ `Supported chains: ${Object.keys(types_1.CHAIN_IDS).join(', ')}`, chain);
104
+ }
105
+ const entryPoint = options.entryPoint || types_1.ENTRYPOINT_V07_ADDRESS;
106
+ const factory = options.factory || types_1.FACTORY_ADDRESSES[network] || types_1.FACTORY_ADDRESSES.mainnet;
107
+ const bundlerUrl = options.bundlerUrl || types_1.DEFAULT_BUNDLER_URLS[chain] || '';
108
+ if (!bundlerUrl) {
109
+ throw new errors_1.Sequence0Error(`No default bundler URL for chain '${chain}'. ` +
110
+ 'Provide a bundlerUrl in options.');
111
+ }
112
+ // Initialize the Sequence0 SDK
113
+ const s0 = new client_1.Sequence0({
114
+ network,
115
+ agentUrl: options.agentUrl,
116
+ ownerPrivateKey: options.ownerPrivateKey,
117
+ });
118
+ // Create a FROST wallet via DKG ceremony
119
+ const wallet = await s0.createWallet({
120
+ chain: chain,
121
+ threshold: { t: options.threshold, n: options.committeeSize },
122
+ curve: 'secp256k1',
123
+ });
124
+ const groupPublicKey = wallet.address; // The FROST group verifying key (hex)
125
+ const walletId = wallet.walletId;
126
+ // Compute the counterfactual smart account address
127
+ const accountAddress = computeCounterfactualAddress(factory, groupPublicKey, entryPoint);
128
+ // Set up RPC provider
129
+ const rpcUrl = options.rpcUrl || getDefaultRpcUrl(chain);
130
+ const provider = new ethers_1.JsonRpcProvider(rpcUrl);
131
+ // Check if already deployed
132
+ const code = await provider.getCode(accountAddress);
133
+ const deployed = code !== '0x' && code !== '0x0';
134
+ return new Sequence0Account({
135
+ accountAddress,
136
+ groupPublicKey,
137
+ walletId,
138
+ s0,
139
+ chain,
140
+ chainId,
141
+ entryPoint,
142
+ factory,
143
+ provider,
144
+ bundlerUrl,
145
+ deployed,
146
+ });
147
+ }
148
+ /**
149
+ * Reconnect to an existing Sequence0 AA wallet by its wallet ID and group public key.
150
+ *
151
+ * Use this when you have previously created an account and want to
152
+ * reconnect without running DKG again.
153
+ *
154
+ * @param walletId - The Sequence0 wallet ID
155
+ * @param groupPublicKey - The FROST group public key (hex)
156
+ * @param options - Account options (chain, network, etc.)
157
+ * @returns A Sequence0Account instance
158
+ */
159
+ static async fromExisting(walletId, groupPublicKey, options) {
160
+ const network = options.network || 'mainnet';
161
+ const chain = options.chain || 'ethereum';
162
+ const chainId = types_1.CHAIN_IDS[chain];
163
+ if (!chainId) {
164
+ throw new errors_1.ChainError(`Unsupported chain: '${chain}'`, chain);
165
+ }
166
+ const entryPoint = options.entryPoint || types_1.ENTRYPOINT_V07_ADDRESS;
167
+ const factory = options.factory || types_1.FACTORY_ADDRESSES[network] || types_1.FACTORY_ADDRESSES.mainnet;
168
+ const bundlerUrl = options.bundlerUrl || types_1.DEFAULT_BUNDLER_URLS[chain] || '';
169
+ const s0 = new client_1.Sequence0({
170
+ network,
171
+ agentUrl: options.agentUrl,
172
+ ownerPrivateKey: options.ownerPrivateKey,
173
+ });
174
+ const accountAddress = computeCounterfactualAddress(factory, groupPublicKey, entryPoint);
175
+ const rpcUrl = options.rpcUrl || getDefaultRpcUrl(chain);
176
+ const provider = new ethers_1.JsonRpcProvider(rpcUrl);
177
+ const code = await provider.getCode(accountAddress);
178
+ const deployed = code !== '0x' && code !== '0x0';
179
+ return new Sequence0Account({
180
+ accountAddress,
181
+ groupPublicKey,
182
+ walletId,
183
+ s0,
184
+ chain,
185
+ chainId,
186
+ entryPoint,
187
+ factory,
188
+ provider,
189
+ bundlerUrl,
190
+ deployed,
191
+ });
192
+ }
193
+ // ────────────────────────────────────────────────
194
+ // Getters
195
+ // ────────────────────────────────────────────────
196
+ /**
197
+ * Get the smart account address (counterfactual or deployed).
198
+ *
199
+ * This address is deterministic: it is derived from the FROST group
200
+ * public key and the factory address via CREATE2. It is known before
201
+ * the account is deployed on-chain.
202
+ */
203
+ getAddress() {
204
+ return this.accountAddress;
205
+ }
206
+ /** Get the FROST wallet ID */
207
+ getWalletId() {
208
+ return this.walletId;
209
+ }
210
+ /** Get the FROST group public key */
211
+ getGroupPublicKey() {
212
+ return this.groupPublicKey;
213
+ }
214
+ /** Get the target chain name */
215
+ getChain() {
216
+ return this.chain;
217
+ }
218
+ /** Get the target chain ID */
219
+ getChainId() {
220
+ return this.chainId;
221
+ }
222
+ /** Check if the account has been deployed on-chain */
223
+ isDeployed() {
224
+ return this.deployed;
225
+ }
226
+ // ────────────────────────────────────────────────
227
+ // Send Transactions
228
+ // ────────────────────────────────────────────────
229
+ /**
230
+ * Build, sign, and submit a single transaction as a UserOperation.
231
+ *
232
+ * If the account is not yet deployed, the initCode is automatically
233
+ * included to deploy it in the same transaction.
234
+ *
235
+ * @param tx - Transaction to execute
236
+ * @returns Transaction hash from the bundler
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const txHash = await account.sendTransaction({
241
+ * to: '0xRecipient',
242
+ * value: BigInt('1000000000000000000'), // 1 ETH
243
+ * });
244
+ * ```
245
+ */
246
+ async sendTransaction(tx) {
247
+ const callData = encodeExecute(tx);
248
+ return this.buildSignAndSubmit(callData);
249
+ }
250
+ /**
251
+ * Build, sign, and submit multiple calls as a single batched UserOperation.
252
+ *
253
+ * All calls are executed atomically in a single transaction.
254
+ * If any call reverts, the entire batch reverts.
255
+ *
256
+ * @param txs - Array of transactions to batch
257
+ * @returns Transaction hash from the bundler
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * const txHash = await account.sendBatchTransaction([
262
+ * { to: '0xTokenA', data: '0xa9059cbb...' }, // ERC-20 transfer
263
+ * { to: '0xTokenB', data: '0xa9059cbb...' }, // Another transfer
264
+ * ]);
265
+ * ```
266
+ */
267
+ async sendBatchTransaction(txs) {
268
+ if (txs.length === 0) {
269
+ throw new errors_1.Sequence0Error('Cannot send empty batch transaction');
270
+ }
271
+ if (txs.length === 1) {
272
+ return this.sendTransaction(txs[0]);
273
+ }
274
+ const callData = encodeExecuteBatch(txs);
275
+ return this.buildSignAndSubmit(callData);
276
+ }
277
+ // ────────────────────────────────────────────────
278
+ // Message Signing
279
+ // ────────────────────────────────────────────────
280
+ /**
281
+ * Sign an arbitrary message using the FROST threshold signing network.
282
+ *
283
+ * The message is hashed with EIP-191 prefix before signing.
284
+ * The resulting signature is a 64-byte Schnorr signature.
285
+ *
286
+ * @param message - Message to sign (string or bytes)
287
+ * @returns Hex-encoded Schnorr signature
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const sig = await account.signMessage('Hello from Sequence0 AA!');
292
+ * ```
293
+ */
294
+ async signMessage(message) {
295
+ const msgBytes = typeof message === 'string'
296
+ ? new TextEncoder().encode(message)
297
+ : message;
298
+ // EIP-191 prefix
299
+ const prefix = `\x19Ethereum Signed Message:\n${msgBytes.length}`;
300
+ const prefixed = new Uint8Array([
301
+ ...new TextEncoder().encode(prefix),
302
+ ...msgBytes,
303
+ ]);
304
+ // keccak256 hash
305
+ const hash = (0, ethers_1.keccak256)(prefixed);
306
+ const hashHex = hash.startsWith('0x') ? hash.slice(2) : hash;
307
+ // Sign via FROST
308
+ return this.s0.signAndWait(this.walletId, hashHex);
309
+ }
310
+ // ────────────────────────────────────────────────
311
+ // UserOperation Building
312
+ // ────────────────────────────────────────────────
313
+ /**
314
+ * Build a UserOperation without signing or submitting.
315
+ *
316
+ * Useful for gas estimation or inspection before submission.
317
+ *
318
+ * @param callData - Encoded call data for the account
319
+ * @returns A PackedUserOperation (without signature)
320
+ */
321
+ async buildUserOp(callData) {
322
+ // Get nonce from EntryPoint
323
+ const nonce = await this.getNonce();
324
+ // Get fee data from the chain
325
+ const feeData = await this.provider.getFeeData();
326
+ const maxFeePerGas = feeData.maxFeePerGas || 30000000000n;
327
+ const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas || 1500000000n;
328
+ // Build init code if not deployed
329
+ let initCode = '0x';
330
+ if (!this.deployed) {
331
+ initCode = this.computeInitCode();
332
+ }
333
+ // Default gas limits (will be refined by bundler estimation)
334
+ const callGasLimit = 200000n;
335
+ const verificationGasLimit = this.deployed ? 100000n : 500000n; // Higher for deployment
336
+ const preVerificationGas = 50000n;
337
+ // Pack gas fields into bytes32
338
+ const accountGasLimits = packGasLimits(verificationGasLimit, callGasLimit);
339
+ const gasFees = packGasFees(maxPriorityFeePerGas, maxFeePerGas);
340
+ return {
341
+ sender: this.accountAddress,
342
+ nonce: '0x' + nonce.toString(16),
343
+ initCode,
344
+ callData,
345
+ accountGasLimits,
346
+ preVerificationGas: '0x' + preVerificationGas.toString(16),
347
+ gasFees,
348
+ paymasterAndData: '0x',
349
+ signature: '0x',
350
+ };
351
+ }
352
+ /**
353
+ * Submit a pre-built and signed UserOperation to a bundler.
354
+ *
355
+ * @param userOp - The signed UserOperation
356
+ * @param bundlerUrl - Optional bundler URL override
357
+ * @returns UserOperation hash from the bundler
358
+ */
359
+ async submitUserOp(userOp, bundlerUrl) {
360
+ const url = bundlerUrl || this.bundlerUrl;
361
+ if (!url) {
362
+ throw new errors_1.Sequence0Error('No bundler URL configured');
363
+ }
364
+ const response = await bundlerRpc(url, 'eth_sendUserOperation', [
365
+ userOpToRpc(userOp),
366
+ this.entryPoint,
367
+ ]);
368
+ // Mark as deployed after first successful submission
369
+ if (!this.deployed && userOp.initCode !== '0x') {
370
+ this.deployed = true;
371
+ }
372
+ return response;
373
+ }
374
+ /**
375
+ * Estimate gas for a UserOperation via the bundler.
376
+ *
377
+ * @param userOp - The UserOperation to estimate
378
+ * @param bundlerUrl - Optional bundler URL override
379
+ * @returns Gas estimates
380
+ */
381
+ async estimateUserOpGas(userOp, bundlerUrl) {
382
+ const url = bundlerUrl || this.bundlerUrl;
383
+ if (!url) {
384
+ throw new errors_1.Sequence0Error('No bundler URL configured');
385
+ }
386
+ return bundlerRpc(url, 'eth_estimateUserOperationGas', [userOpToRpc(userOp), this.entryPoint]);
387
+ }
388
+ /**
389
+ * Get the receipt for a submitted UserOperation.
390
+ *
391
+ * @param userOpHash - The UserOperation hash from submitUserOp
392
+ * @param bundlerUrl - Optional bundler URL override
393
+ * @returns Receipt if the UserOp has been included, null otherwise
394
+ */
395
+ async getUserOpReceipt(userOpHash, bundlerUrl) {
396
+ const url = bundlerUrl || this.bundlerUrl;
397
+ if (!url) {
398
+ throw new errors_1.Sequence0Error('No bundler URL configured');
399
+ }
400
+ try {
401
+ return await bundlerRpc(url, 'eth_getUserOperationReceipt', [userOpHash]);
402
+ }
403
+ catch {
404
+ return null;
405
+ }
406
+ }
407
+ /**
408
+ * Wait for a UserOperation to be included on-chain.
409
+ *
410
+ * Polls the bundler for the receipt until it appears or timeout.
411
+ *
412
+ * @param userOpHash - The UserOperation hash
413
+ * @param timeoutMs - Timeout in milliseconds (default: 60000)
414
+ * @param pollIntervalMs - Poll interval (default: 2000)
415
+ * @returns The UserOperation receipt
416
+ */
417
+ async waitForUserOp(userOpHash, timeoutMs = 60000, pollIntervalMs = 2000) {
418
+ const deadline = Date.now() + timeoutMs;
419
+ while (Date.now() < deadline) {
420
+ const receipt = await this.getUserOpReceipt(userOpHash);
421
+ if (receipt) {
422
+ return receipt;
423
+ }
424
+ await sleep(pollIntervalMs);
425
+ }
426
+ throw new errors_1.Sequence0Error(`UserOperation ${userOpHash} was not included within ${timeoutMs}ms`);
427
+ }
428
+ // ────────────────────────────────────────────────
429
+ // Balance & Info
430
+ // ────────────────────────────────────────────────
431
+ /**
432
+ * Get the native token balance of the smart account.
433
+ *
434
+ * @returns Balance in wei as a string
435
+ */
436
+ async getBalance() {
437
+ const balance = await this.provider.getBalance(this.accountAddress);
438
+ return balance.toString();
439
+ }
440
+ /**
441
+ * Get account information summary.
442
+ */
443
+ info() {
444
+ return {
445
+ address: this.accountAddress,
446
+ walletId: this.walletId,
447
+ groupPublicKey: this.groupPublicKey,
448
+ chain: this.chain,
449
+ chainId: this.chainId,
450
+ entryPoint: this.entryPoint,
451
+ factory: this.factory,
452
+ deployed: this.deployed,
453
+ };
454
+ }
455
+ /**
456
+ * Clean up resources.
457
+ */
458
+ destroy() {
459
+ this.s0.destroy();
460
+ }
461
+ // ────────────────────────────────────────────────
462
+ // Internal
463
+ // ────────────────────────────────────────────────
464
+ /**
465
+ * Build, sign, and submit a UserOperation.
466
+ */
467
+ async buildSignAndSubmit(callData) {
468
+ // Build the unsigned UserOperation
469
+ const userOp = await this.buildUserOp(callData);
470
+ // Estimate gas via bundler (optional, improves success rate)
471
+ try {
472
+ const gasEstimate = await this.estimateUserOpGas(userOp);
473
+ if (gasEstimate.callGasLimit) {
474
+ const callGas = BigInt(gasEstimate.callGasLimit);
475
+ const verifyGas = BigInt(gasEstimate.verificationGasLimit);
476
+ userOp.accountGasLimits = packGasLimits(verifyGas, callGas);
477
+ }
478
+ if (gasEstimate.preVerificationGas) {
479
+ userOp.preVerificationGas = gasEstimate.preVerificationGas;
480
+ }
481
+ }
482
+ catch {
483
+ // Use default gas estimates if bundler estimation fails
484
+ }
485
+ // Compute the UserOperation hash
486
+ const userOpHash = computeUserOpHash(userOp, this.entryPoint, this.chainId);
487
+ // Sign the hash via FROST
488
+ const hashHex = userOpHash.startsWith('0x') ? userOpHash.slice(2) : userOpHash;
489
+ const signature = await this.s0.signAndWait(this.walletId, hashHex);
490
+ // Encode the FROST signature for on-chain verification
491
+ userOp.signature = encodeSchnorrSignature(signature);
492
+ // Submit to bundler
493
+ return this.submitUserOp(userOp);
494
+ }
495
+ /**
496
+ * Get the current nonce from the EntryPoint contract.
497
+ */
498
+ async getNonce() {
499
+ try {
500
+ // eth_call to EntryPoint.getNonce(sender, key)
501
+ // key = 0 for the default nonce sequence
502
+ const senderPadded = this.accountAddress.slice(2).toLowerCase().padStart(64, '0');
503
+ const keyPadded = '0'.padStart(64, '0');
504
+ const calldata = GET_NONCE_SELECTOR + senderPadded + keyPadded;
505
+ const result = await this.provider.call({
506
+ to: this.entryPoint,
507
+ data: calldata,
508
+ });
509
+ if (!result || result === '0x')
510
+ return 0n;
511
+ return BigInt(result);
512
+ }
513
+ catch {
514
+ return 0n;
515
+ }
516
+ }
517
+ /**
518
+ * Compute the initCode for first-time account deployment.
519
+ *
520
+ * initCode = factoryAddress (20 bytes) + createAccount(groupPublicKey) calldata
521
+ */
522
+ computeInitCode() {
523
+ const pubkeyHex = this.groupPublicKey.startsWith('0x')
524
+ ? this.groupPublicKey.slice(2)
525
+ : this.groupPublicKey;
526
+ // Compute function selector
527
+ const selectorBytes = (0, ethers_1.keccak256)(new TextEncoder().encode(CREATE_ACCOUNT_SELECTOR_HASH));
528
+ const selector = selectorBytes.slice(0, 10); // 0x + 4 bytes
529
+ // ABI-encode: offset(32) + length(32) + padded_data
530
+ const pubkeyBytes = hexToBytes(pubkeyHex);
531
+ const paddedLen = Math.ceil(pubkeyBytes.length / 32) * 32;
532
+ const offsetHex = '0000000000000000000000000000000000000000000000000000000000000020';
533
+ const lengthHex = pubkeyBytes.length.toString(16).padStart(64, '0');
534
+ const dataHex = pubkeyHex.padEnd(paddedLen * 2, '0');
535
+ const factoryHex = this.factory.startsWith('0x')
536
+ ? this.factory.slice(2)
537
+ : this.factory;
538
+ return '0x' + factoryHex + selector.slice(2) + offsetHex + lengthHex + dataHex;
539
+ }
540
+ }
541
+ exports.Sequence0Account = Sequence0Account;
542
+ // ────────────────────────────────────────────────
543
+ // Pure Utility Functions
544
+ // ────────────────────────────────────────────────
545
+ /**
546
+ * Compute the counterfactual smart account address using CREATE2.
547
+ *
548
+ * address = keccak256(0xff + factory + salt + keccak256(initCode))[12:]
549
+ *
550
+ * The salt is derived from the group public key.
551
+ */
552
+ function computeCounterfactualAddress(factory, groupPublicKey, _entryPoint) {
553
+ // The salt is the keccak256 of the group public key
554
+ const pubkeyHex = groupPublicKey.startsWith('0x')
555
+ ? groupPublicKey.slice(2)
556
+ : groupPublicKey;
557
+ const salt = (0, ethers_1.keccak256)(hexToBytes(pubkeyHex));
558
+ // For counterfactual address computation, we use a simplified approach:
559
+ // The actual address depends on the factory's CREATE2 implementation.
560
+ // This computes: keccak256(0xff || factory || salt || initCodeHash)
561
+ // where initCodeHash would be the hash of the account's creation code.
562
+ //
563
+ // In practice, the factory contract pre-computes this for us.
564
+ // For now, we use a deterministic derivation from the factory + pubkey.
565
+ const factoryHex = factory.startsWith('0x') ? factory.slice(2) : factory;
566
+ const saltHex = salt.startsWith('0x') ? salt.slice(2) : salt;
567
+ // Prefix: 0xff
568
+ const payload = '0xff' + factoryHex + saltHex;
569
+ const hash = (0, ethers_1.keccak256)(hexToBytes(payload));
570
+ // Take last 20 bytes
571
+ const addressHex = hash.startsWith('0x') ? hash.slice(26) : hash.slice(24);
572
+ return '0x' + addressHex;
573
+ }
574
+ /**
575
+ * Compute the ERC-4337 userOpHash.
576
+ *
577
+ * userOpHash = keccak256(
578
+ * keccak256(pack(userOp)),
579
+ * entryPoint,
580
+ * chainId
581
+ * )
582
+ */
583
+ function computeUserOpHash(userOp, entryPoint, chainId) {
584
+ // Step 1: Hash dynamic fields
585
+ const initCodeHash = (0, ethers_1.keccak256)(hexToBytes(userOp.initCode || '0x'));
586
+ const callDataHash = (0, ethers_1.keccak256)(hexToBytes(userOp.callData || '0x'));
587
+ const paymasterHash = (0, ethers_1.keccak256)(hexToBytes(userOp.paymasterAndData || '0x'));
588
+ // Step 2: Pack the UserOp fields
589
+ const senderPadded = userOp.sender.slice(2).toLowerCase().padStart(64, '0');
590
+ const noncePadded = padHex(userOp.nonce, 64);
591
+ const initCodeHashHex = initCodeHash.startsWith('0x') ? initCodeHash.slice(2) : initCodeHash;
592
+ const callDataHashHex = callDataHash.startsWith('0x') ? callDataHash.slice(2) : callDataHash;
593
+ const accountGasHex = padHex(userOp.accountGasLimits, 64);
594
+ const preVerGasHex = padHex(userOp.preVerificationGas, 64);
595
+ const gasFeesHex = padHex(userOp.gasFees, 64);
596
+ const paymasterHashHex = paymasterHash.startsWith('0x') ? paymasterHash.slice(2) : paymasterHash;
597
+ const packed = senderPadded +
598
+ noncePadded +
599
+ initCodeHashHex +
600
+ callDataHashHex +
601
+ accountGasHex +
602
+ preVerGasHex +
603
+ gasFeesHex +
604
+ paymasterHashHex;
605
+ const packedHash = (0, ethers_1.keccak256)(hexToBytes('0x' + packed));
606
+ // Step 3: Final hash
607
+ const entryPointPadded = entryPoint.slice(2).toLowerCase().padStart(64, '0');
608
+ const chainIdHex = chainId.toString(16).padStart(64, '0');
609
+ const packedHashHex = packedHash.startsWith('0x') ? packedHash.slice(2) : packedHash;
610
+ const finalInput = packedHashHex + entryPointPadded + chainIdHex;
611
+ return (0, ethers_1.keccak256)(hexToBytes('0x' + finalInput));
612
+ }
613
+ /**
614
+ * Encode a FROST Schnorr signature for on-chain verification.
615
+ *
616
+ * The FROST aggregate signature is serialized as JSON containing the
617
+ * R point and z scalar. We extract R_x (32 bytes) and z (32 bytes)
618
+ * for the on-chain Schnorr verifier.
619
+ *
620
+ * If the signature is already a raw 64-byte hex string, it is used as-is.
621
+ */
622
+ function encodeSchnorrSignature(signatureHex) {
623
+ const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
624
+ // If already 64 bytes (128 hex chars), it's a raw Schnorr signature
625
+ if (sig.length === 128) {
626
+ return '0x' + sig;
627
+ }
628
+ // Try to parse as JSON (serialized FROST signature)
629
+ try {
630
+ const bytes = hexToBytes(sig);
631
+ const text = new TextDecoder().decode(bytes);
632
+ const parsed = JSON.parse(text);
633
+ // Extract R_x and z from the FROST signature structure
634
+ if (parsed.R && parsed.z) {
635
+ const rHex = parsed.R.startsWith('0x') ? parsed.R.slice(2) : parsed.R;
636
+ const zHex = parsed.z.startsWith('0x') ? parsed.z.slice(2) : parsed.z;
637
+ // R might be a compressed point (33 bytes) -- take x coordinate
638
+ const rBytes = rHex.length === 66 ? rHex.slice(2) : rHex;
639
+ return '0x' + rBytes.padStart(64, '0') + zHex.padStart(64, '0');
640
+ }
641
+ }
642
+ catch {
643
+ // Not JSON, try other formats
644
+ }
645
+ // If 65 bytes (130 hex chars), might be ECDSA-style R+S+V -- take first 64 bytes
646
+ if (sig.length === 130) {
647
+ return '0x' + sig.slice(0, 128);
648
+ }
649
+ // Return as-is with 0x prefix
650
+ return '0x' + sig;
651
+ }
652
+ /**
653
+ * Encode a single execute() call for the smart account.
654
+ *
655
+ * execute(address dest, uint256 value, bytes func)
656
+ */
657
+ function encodeExecute(tx) {
658
+ const dest = tx.to.slice(2).toLowerCase().padStart(64, '0');
659
+ const value = (tx.value ? BigInt(tx.value) : 0n).toString(16).padStart(64, '0');
660
+ const funcData = tx.data
661
+ ? (tx.data.startsWith('0x') ? tx.data.slice(2) : tx.data)
662
+ : '';
663
+ // Dynamic bytes: offset + length + padded data
664
+ const offset = (3 * 32).toString(16).padStart(64, '0'); // 3 words = 96 bytes
665
+ const funcLen = (funcData.length / 2).toString(16).padStart(64, '0');
666
+ const paddedFunc = funcData.padEnd(Math.ceil(funcData.length / 64) * 64, '0');
667
+ return EXECUTE_SELECTOR + dest + value + offset + funcLen + paddedFunc;
668
+ }
669
+ /**
670
+ * Encode a batch executeBatch() call for the smart account.
671
+ *
672
+ * executeBatch(address[] dest, uint256[] values, bytes[] func)
673
+ */
674
+ function encodeExecuteBatch(txs) {
675
+ const count = txs.length;
676
+ // We need to ABI-encode three dynamic arrays: address[], uint256[], bytes[]
677
+ // Layout: selector + offset_dest + offset_values + offset_func + data...
678
+ // Compute offsets (each is 32 bytes from the start of the params)
679
+ const headerWords = 3; // 3 offset words
680
+ const destArrayWords = 1 + count; // length + count addresses
681
+ const valuesArrayWords = 1 + count; // length + count uint256s
682
+ const destOffset = headerWords * 32;
683
+ const valuesOffset = destOffset + destArrayWords * 32;
684
+ const funcOffset = valuesOffset + valuesArrayWords * 32;
685
+ let result = EXECUTE_BATCH_SELECTOR;
686
+ // Offsets
687
+ result += destOffset.toString(16).padStart(64, '0');
688
+ result += valuesOffset.toString(16).padStart(64, '0');
689
+ result += funcOffset.toString(16).padStart(64, '0');
690
+ // address[] dest
691
+ result += count.toString(16).padStart(64, '0');
692
+ for (const tx of txs) {
693
+ result += tx.to.slice(2).toLowerCase().padStart(64, '0');
694
+ }
695
+ // uint256[] values
696
+ result += count.toString(16).padStart(64, '0');
697
+ for (const tx of txs) {
698
+ const val = tx.value ? BigInt(tx.value) : 0n;
699
+ result += val.toString(16).padStart(64, '0');
700
+ }
701
+ // bytes[] func
702
+ result += count.toString(16).padStart(64, '0');
703
+ // Compute offsets for each bytes element (relative to array data start)
704
+ const funcDataParts = [];
705
+ let currentOffset = count * 32; // skip the offset words
706
+ for (const tx of txs) {
707
+ result += currentOffset.toString(16).padStart(64, '0');
708
+ const data = tx.data
709
+ ? (tx.data.startsWith('0x') ? tx.data.slice(2) : tx.data)
710
+ : '';
711
+ const paddedData = data.padEnd(Math.ceil(Math.max(data.length, 1) / 64) * 64, '0');
712
+ const dataWords = 1 + paddedData.length / 64; // length word + padded data
713
+ currentOffset += dataWords * 32;
714
+ funcDataParts.push(data);
715
+ }
716
+ // Encode each bytes element
717
+ for (const data of funcDataParts) {
718
+ const dataLen = data.length / 2;
719
+ result += dataLen.toString(16).padStart(64, '0');
720
+ if (data.length > 0) {
721
+ result += data.padEnd(Math.ceil(data.length / 64) * 64, '0');
722
+ }
723
+ }
724
+ return result;
725
+ }
726
+ /** Convert a UserOperation to the RPC format expected by bundlers */
727
+ function userOpToRpc(userOp) {
728
+ return {
729
+ sender: userOp.sender,
730
+ nonce: userOp.nonce,
731
+ initCode: userOp.initCode || '0x',
732
+ callData: userOp.callData,
733
+ accountGasLimits: userOp.accountGasLimits,
734
+ preVerificationGas: userOp.preVerificationGas,
735
+ gasFees: userOp.gasFees,
736
+ paymasterAndData: userOp.paymasterAndData || '0x',
737
+ signature: userOp.signature,
738
+ };
739
+ }
740
+ /** Make a JSON-RPC call to a bundler */
741
+ async function bundlerRpc(url, method, params) {
742
+ const response = await fetch(url, {
743
+ method: 'POST',
744
+ headers: { 'Content-Type': 'application/json' },
745
+ body: JSON.stringify({
746
+ jsonrpc: '2.0',
747
+ method,
748
+ params,
749
+ id: Date.now(),
750
+ }),
751
+ });
752
+ if (!response.ok) {
753
+ const text = await response.text();
754
+ throw new errors_1.Sequence0Error(`Bundler request failed: HTTP ${response.status} - ${text}`);
755
+ }
756
+ const json = await response.json();
757
+ if (json.error) {
758
+ throw new errors_1.Sequence0Error(`Bundler error: ${json.error.message} (code: ${json.error.code})`);
759
+ }
760
+ return json.result;
761
+ }
762
+ /** Pack gas limits into bytes32: verificationGasLimit (16 bytes) || callGasLimit (16 bytes) */
763
+ function packGasLimits(verificationGasLimit, callGasLimit) {
764
+ const verify = verificationGasLimit.toString(16).padStart(32, '0');
765
+ const call = callGasLimit.toString(16).padStart(32, '0');
766
+ return '0x' + verify + call;
767
+ }
768
+ /** Pack gas fees into bytes32: maxPriorityFeePerGas (16 bytes) || maxFeePerGas (16 bytes) */
769
+ function packGasFees(maxPriorityFeePerGas, maxFeePerGas) {
770
+ const priority = maxPriorityFeePerGas.toString(16).padStart(32, '0');
771
+ const max = maxFeePerGas.toString(16).padStart(32, '0');
772
+ return '0x' + priority + max;
773
+ }
774
+ /** Pad a hex string to a given length (removing 0x prefix) */
775
+ function padHex(hex, length) {
776
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
777
+ return clean.padStart(length, '0');
778
+ }
779
+ /** Hex string to Uint8Array */
780
+ function hexToBytes(hex) {
781
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
782
+ if (clean.length === 0)
783
+ return new Uint8Array(0);
784
+ const bytes = new Uint8Array(clean.length / 2);
785
+ for (let i = 0; i < bytes.length; i++) {
786
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
787
+ }
788
+ return bytes;
789
+ }
790
+ /** Get default RPC URL for a chain */
791
+ function getDefaultRpcUrl(chain) {
792
+ const urls = {
793
+ ethereum: 'https://eth.llamarpc.com',
794
+ polygon: 'https://polygon-rpc.com',
795
+ bsc: 'https://bsc-dataseed.binance.org',
796
+ avalanche: 'https://api.avax.network/ext/bc/C/rpc',
797
+ arbitrum: 'https://arb1.arbitrum.io/rpc',
798
+ optimism: 'https://mainnet.optimism.io',
799
+ base: 'https://mainnet.base.org',
800
+ scroll: 'https://rpc.scroll.io',
801
+ linea: 'https://rpc.linea.build',
802
+ mantle: 'https://rpc.mantle.xyz',
803
+ blast: 'https://rpc.blast.io',
804
+ };
805
+ return urls[chain] || urls.ethereum;
806
+ }
807
+ function sleep(ms) {
808
+ return new Promise((resolve) => setTimeout(resolve, ms));
809
+ }
810
+ //# sourceMappingURL=account.js.map