@1llet.xyz/erc4337-gasless-sdk 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -20
- package/package.json +1 -1
- package/src/AccountAbstraction.ts +148 -405
- package/src/TokenService.ts +92 -0
- package/src/UserOpBuilder.ts +173 -0
- package/src/chains.ts +54 -0
- package/src/index.ts +12 -4
- package/src/types.ts +7 -1
- package/src/deployments.ts +0 -22
package/README.md
CHANGED
|
@@ -17,20 +17,14 @@ yarn add @1llet.xyz/erc4337-gasless-sdk viem
|
|
|
17
17
|
Define the chain configuration (including your Bundler URL and Paymaster).
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// rpcUrl: "https://sepolia.base.org",
|
|
29
|
-
|
|
30
|
-
// Addresses are automatically resolved for supported chains (Base, Base Sepolia)
|
|
31
|
-
// You can override them if needed:
|
|
32
|
-
// factoryAddress: "0x...",
|
|
33
|
-
};
|
|
20
|
+
// 1. Import Config (Chain Registry)
|
|
21
|
+
import { BASE_SEPOLIA, type ChainConfig } from "@1llet.xyz/erc4337-gasless-sdk";
|
|
22
|
+
import { AccountAbstraction } from "@1llet.xyz/erc4337-gasless-sdk";
|
|
23
|
+
|
|
24
|
+
// 2. Initialize
|
|
25
|
+
const aa = new AccountAbstraction(BASE_SEPOLIA);
|
|
26
|
+
|
|
27
|
+
await aa.connect();
|
|
34
28
|
```
|
|
35
29
|
|
|
36
30
|
### 2. Initialize & Connect
|
|
@@ -163,14 +157,31 @@ const receipt = await aa.sendBatchTransaction([
|
|
|
163
157
|
]);
|
|
164
158
|
|
|
165
159
|
// 3. Transfer ERC-20 Tokens (Helper)
|
|
166
|
-
// Automatically encodes the
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
// Automatically encodes the
|
|
161
|
+
// 1. Transfer ERC-20 (USDC)
|
|
162
|
+
await aa.transfer("USDC", recipient, amount);
|
|
163
|
+
|
|
164
|
+
// 2. Transfer Native Token (ETH)
|
|
165
|
+
// The SDK detects the "ETH" symbol and sends a native transaction
|
|
166
|
+
await aa.transfer("ETH", recipient, amount);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
// 2. Transfer Native Token (ETH)
|
|
170
|
+
// The SDK detects the "ETH" symbol and sends a native transaction
|
|
171
|
+
await aa.transfer("ETH", recipient, amount);
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
### Funding the Account
|
|
175
|
+
|
|
176
|
+
Easily deposit ETH from the connected wallet (EOA) to the Smart Account.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Deposit 0.1 ETH
|
|
180
|
+
const txHash = await aa.deposit(100000000000000000n);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### High-Level Methods
|
|
184
|
+
|
|
174
185
|
### Error Decoding
|
|
175
186
|
The SDK now automatically tries to decode cryptic "0x..." errors from the EntryPoint into readable messages like:
|
|
176
187
|
- `Smart Account Error: Transfer amount exceeds balance`
|
package/package.json
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createPublicClient,
|
|
3
3
|
http,
|
|
4
|
-
encodeFunctionData,
|
|
5
|
-
encodeAbiParameters,
|
|
6
|
-
keccak256,
|
|
7
4
|
type Address,
|
|
8
5
|
type Hash,
|
|
9
6
|
type Hex,
|
|
@@ -12,19 +9,17 @@ import {
|
|
|
12
9
|
} from "viem";
|
|
13
10
|
import {
|
|
14
11
|
factoryAbi,
|
|
15
|
-
entryPointAbi,
|
|
16
|
-
smartAccountAbi,
|
|
17
|
-
erc20Abi,
|
|
18
12
|
} from "./constants";
|
|
19
13
|
import {
|
|
20
14
|
type ChainConfig,
|
|
21
15
|
type UserOperation,
|
|
22
|
-
type GasEstimate,
|
|
23
16
|
type UserOpReceipt,
|
|
24
|
-
type ApprovalSupportResult
|
|
17
|
+
type ApprovalSupportResult,
|
|
18
|
+
type Token
|
|
25
19
|
} from "./types";
|
|
26
|
-
import { DEPLOYMENTS } from "./deployments";
|
|
27
20
|
import { BundlerClient } from "./BundlerClient";
|
|
21
|
+
import { TokenService } from "./TokenService";
|
|
22
|
+
import { UserOpBuilder } from "./UserOpBuilder";
|
|
28
23
|
|
|
29
24
|
/**
|
|
30
25
|
* ERC-4337 Account Abstraction Client
|
|
@@ -36,41 +31,35 @@ export class AccountAbstraction {
|
|
|
36
31
|
private publicClient: PublicClient;
|
|
37
32
|
private bundlerClient: BundlerClient;
|
|
38
33
|
|
|
34
|
+
// Services
|
|
35
|
+
private tokenService: TokenService;
|
|
36
|
+
private userOpBuilder: UserOpBuilder;
|
|
37
|
+
|
|
39
38
|
// Resolved addresses
|
|
40
39
|
private entryPointAddress: Address;
|
|
41
40
|
private factoryAddress: Address;
|
|
42
|
-
private paymasterAddress?: Address;
|
|
43
|
-
private usdcAddress: Address;
|
|
44
41
|
|
|
45
42
|
constructor(chainConfig: ChainConfig) {
|
|
46
43
|
this.chainConfig = chainConfig;
|
|
47
|
-
const chainId = chainConfig.chain.id;
|
|
48
|
-
const defaults = DEPLOYMENTS[chainId];
|
|
49
|
-
|
|
50
|
-
// Resolve addresses (Config > Defaults > Error)
|
|
51
|
-
const entryPoint = chainConfig.entryPointAddress || defaults?.entryPoint;
|
|
52
|
-
if (!entryPoint) throw new Error(`EntryPoint address not found for chain ${chainId}`);
|
|
53
|
-
this.entryPointAddress = entryPoint;
|
|
54
|
-
|
|
55
|
-
const factory = chainConfig.factoryAddress || defaults?.factory;
|
|
56
|
-
if (!factory) throw new Error(`Factory address not found for chain ${chainId}`);
|
|
57
|
-
this.factoryAddress = factory;
|
|
58
|
-
|
|
59
|
-
const usdc = chainConfig.usdcAddress || defaults?.usdc;
|
|
60
|
-
if (!usdc) throw new Error(`USDC address not found for chain ${chainId}`);
|
|
61
|
-
this.usdcAddress = usdc;
|
|
62
44
|
|
|
63
|
-
|
|
45
|
+
// Validation
|
|
46
|
+
if (!chainConfig.entryPointAddress) throw new Error("EntryPoint address required");
|
|
47
|
+
this.entryPointAddress = chainConfig.entryPointAddress;
|
|
48
|
+
if (!chainConfig.factoryAddress) throw new Error("Factory address required");
|
|
49
|
+
this.factoryAddress = chainConfig.factoryAddress;
|
|
64
50
|
|
|
65
|
-
//
|
|
51
|
+
// Setup Clients
|
|
66
52
|
const rpcUrl = chainConfig.rpcUrl || chainConfig.chain.rpcUrls.default.http[0];
|
|
67
|
-
|
|
68
53
|
this.publicClient = createPublicClient({
|
|
69
54
|
chain: chainConfig.chain,
|
|
70
55
|
transport: http(rpcUrl),
|
|
71
56
|
});
|
|
72
57
|
|
|
73
58
|
this.bundlerClient = new BundlerClient(chainConfig, this.entryPointAddress);
|
|
59
|
+
|
|
60
|
+
// Setup Services
|
|
61
|
+
this.tokenService = new TokenService(chainConfig, this.publicClient);
|
|
62
|
+
this.userOpBuilder = new UserOpBuilder(chainConfig, this.bundlerClient, this.publicClient);
|
|
74
63
|
}
|
|
75
64
|
|
|
76
65
|
/**
|
|
@@ -86,19 +75,15 @@ export class AccountAbstraction {
|
|
|
86
75
|
method: "eth_requestAccounts",
|
|
87
76
|
})) as string[];
|
|
88
77
|
|
|
89
|
-
if (!accounts || accounts.length === 0)
|
|
90
|
-
throw new Error("No accounts found");
|
|
91
|
-
}
|
|
78
|
+
if (!accounts || accounts.length === 0) throw new Error("No accounts found");
|
|
92
79
|
|
|
93
80
|
// Check network
|
|
94
81
|
const chainId = (await window.ethereum.request({
|
|
95
82
|
method: "eth_chainId",
|
|
96
83
|
})) as string;
|
|
97
|
-
|
|
98
84
|
const targetChainId = this.chainConfig.chain.id;
|
|
99
85
|
|
|
100
86
|
if (parseInt(chainId, 16) !== targetChainId) {
|
|
101
|
-
// Switch to configured chain
|
|
102
87
|
try {
|
|
103
88
|
await window.ethereum.request({
|
|
104
89
|
method: "wallet_switchEthereumChain",
|
|
@@ -106,7 +91,6 @@ export class AccountAbstraction {
|
|
|
106
91
|
});
|
|
107
92
|
} catch (switchError: unknown) {
|
|
108
93
|
const error = switchError as { code?: number };
|
|
109
|
-
// Chain not added, add it
|
|
110
94
|
if (error.code === 4902) {
|
|
111
95
|
await window.ethereum.request({
|
|
112
96
|
method: "wallet_addEthereumChain",
|
|
@@ -115,7 +99,7 @@ export class AccountAbstraction {
|
|
|
115
99
|
chainId: "0x" + targetChainId.toString(16),
|
|
116
100
|
chainName: this.chainConfig.chain.name,
|
|
117
101
|
nativeCurrency: this.chainConfig.chain.nativeCurrency,
|
|
118
|
-
rpcUrls: [this.chainConfig.rpcUrl],
|
|
102
|
+
rpcUrls: [this.chainConfig.rpcUrl || this.chainConfig.chain.rpcUrls.default.http[0]],
|
|
119
103
|
blockExplorerUrls: this.chainConfig.chain.blockExplorers?.default?.url
|
|
120
104
|
? [this.chainConfig.chain.blockExplorers.default.url]
|
|
121
105
|
: [],
|
|
@@ -138,14 +122,14 @@ export class AccountAbstraction {
|
|
|
138
122
|
}
|
|
139
123
|
|
|
140
124
|
/**
|
|
141
|
-
* Get the Smart Account address for an owner
|
|
125
|
+
* Get the Smart Account address for an owner
|
|
142
126
|
*/
|
|
143
127
|
async getSmartAccountAddress(owner: Address): Promise<Address> {
|
|
144
128
|
const address = await this.publicClient.readContract({
|
|
145
129
|
address: this.factoryAddress,
|
|
146
130
|
abi: factoryAbi,
|
|
147
131
|
functionName: "getAccountAddress",
|
|
148
|
-
args: [owner, 0n],
|
|
132
|
+
args: [owner, 0n],
|
|
149
133
|
}) as Address;
|
|
150
134
|
return address;
|
|
151
135
|
}
|
|
@@ -154,323 +138,58 @@ export class AccountAbstraction {
|
|
|
154
138
|
* Check if the Smart Account is deployed
|
|
155
139
|
*/
|
|
156
140
|
async isAccountDeployed(): Promise<boolean> {
|
|
157
|
-
if (!this.smartAccountAddress)
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const code = await this.publicClient.getCode({
|
|
162
|
-
address: this.smartAccountAddress,
|
|
163
|
-
});
|
|
164
|
-
return code !== undefined && code !== "0x";
|
|
141
|
+
if (!this.smartAccountAddress) throw new Error("Not connected");
|
|
142
|
+
return this.userOpBuilder.isAccountDeployed(this.smartAccountAddress);
|
|
165
143
|
}
|
|
166
144
|
|
|
145
|
+
// --- Token Methods (Delegated) ---
|
|
167
146
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
*/
|
|
171
|
-
async getUsdcBalance(): Promise<bigint> {
|
|
172
|
-
if (!this.smartAccountAddress) {
|
|
173
|
-
throw new Error("Not connected");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return await this.publicClient.readContract({
|
|
177
|
-
address: this.usdcAddress,
|
|
178
|
-
abi: erc20Abi,
|
|
179
|
-
functionName: "balanceOf",
|
|
180
|
-
args: [this.smartAccountAddress],
|
|
181
|
-
}) as bigint;
|
|
147
|
+
getTokenAddress(token: string | Address): Address {
|
|
148
|
+
return this.tokenService.getTokenAddress(token);
|
|
182
149
|
}
|
|
183
150
|
|
|
151
|
+
async getUsdcBalance(): Promise<bigint> {
|
|
152
|
+
if (!this.smartAccountAddress) throw new Error("Not connected");
|
|
153
|
+
return this.tokenService.getBalance("USDC", this.smartAccountAddress);
|
|
154
|
+
}
|
|
184
155
|
|
|
185
|
-
/**
|
|
186
|
-
* Get the EOA's USDC balance
|
|
187
|
-
*/
|
|
188
156
|
async getEoaUsdcBalance(): Promise<bigint> {
|
|
189
|
-
if (!this.owner)
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return await this.publicClient.readContract({
|
|
194
|
-
address: this.usdcAddress,
|
|
195
|
-
abi: erc20Abi,
|
|
196
|
-
functionName: "balanceOf",
|
|
197
|
-
args: [this.owner],
|
|
198
|
-
}) as bigint;
|
|
157
|
+
if (!this.owner) throw new Error("Not connected");
|
|
158
|
+
return this.tokenService.getBalance("USDC", this.owner);
|
|
199
159
|
}
|
|
200
160
|
|
|
201
|
-
/**
|
|
202
|
-
* Get the allowance of the Smart Account to spend the EOA's USDC
|
|
203
|
-
*/
|
|
204
161
|
async getAllowance(): Promise<bigint> {
|
|
205
|
-
if (!this.owner || !this.smartAccountAddress)
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return await this.publicClient.readContract({
|
|
210
|
-
address: this.usdcAddress,
|
|
211
|
-
abi: erc20Abi,
|
|
212
|
-
functionName: "allowance",
|
|
213
|
-
args: [this.owner, this.smartAccountAddress],
|
|
214
|
-
}) as bigint;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Get the nonce for the Smart Account
|
|
219
|
-
*/
|
|
220
|
-
async getNonce(): Promise<bigint> {
|
|
221
|
-
if (!this.smartAccountAddress) {
|
|
222
|
-
throw new Error("Not connected");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return await this.publicClient.readContract({
|
|
226
|
-
address: this.entryPointAddress,
|
|
227
|
-
abi: entryPointAbi,
|
|
228
|
-
functionName: "getNonce",
|
|
229
|
-
args: [this.smartAccountAddress, 0n],
|
|
230
|
-
}) as bigint;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Build initCode for account deployment
|
|
235
|
-
*/
|
|
236
|
-
buildInitCode(): Hex {
|
|
237
|
-
if (!this.owner) {
|
|
238
|
-
throw new Error("Not connected");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const createAccountData = encodeFunctionData({
|
|
242
|
-
abi: factoryAbi,
|
|
243
|
-
functionName: "createAccount",
|
|
244
|
-
args: [this.owner, 0n],
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
return `${this.factoryAddress}${createAccountData.slice(2)}` as Hex;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Estimate gas for a UserOperation
|
|
253
|
-
*/
|
|
254
|
-
async estimateGas(userOp: Partial<UserOperation>): Promise<GasEstimate> {
|
|
255
|
-
return this.bundlerClient.estimateGas(userOp);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Build a UserOperation for Batched Execution (e.g. USDC Transfer + Fee)
|
|
261
|
-
*/
|
|
262
|
-
async buildUserOperationBatch(
|
|
263
|
-
transactions: { target: Address; value: bigint; data: Hex }[]
|
|
264
|
-
): Promise<UserOperation> {
|
|
265
|
-
if (!this.owner || !this.smartAccountAddress) {
|
|
266
|
-
throw new Error("Not connected");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const isDeployed = await this.isAccountDeployed();
|
|
270
|
-
const initCode = isDeployed ? "0x" : this.buildInitCode();
|
|
271
|
-
|
|
272
|
-
// Prepare arrays for executeBatch
|
|
273
|
-
const targets = transactions.map((tx) => tx.target);
|
|
274
|
-
const values = transactions.map((tx) => tx.value);
|
|
275
|
-
const datas = transactions.map((tx) => tx.data);
|
|
276
|
-
|
|
277
|
-
// Encode callData for executeBatch
|
|
278
|
-
const callData = encodeFunctionData({
|
|
279
|
-
abi: smartAccountAbi,
|
|
280
|
-
functionName: "executeBatch",
|
|
281
|
-
args: [targets, values, datas],
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const nonce = await this.getNonce();
|
|
285
|
-
|
|
286
|
-
// Estimate gas
|
|
287
|
-
const gasEstimate = await this.estimateGas({
|
|
288
|
-
sender: this.smartAccountAddress,
|
|
289
|
-
nonce,
|
|
290
|
-
initCode: initCode as Hex,
|
|
291
|
-
callData,
|
|
292
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
sender: this.smartAccountAddress,
|
|
297
|
-
nonce,
|
|
298
|
-
initCode: initCode as Hex,
|
|
299
|
-
callData,
|
|
300
|
-
callGasLimit: BigInt(gasEstimate.callGasLimit),
|
|
301
|
-
verificationGasLimit: BigInt(gasEstimate.verificationGasLimit),
|
|
302
|
-
preVerificationGas: BigInt(gasEstimate.preVerificationGas),
|
|
303
|
-
maxFeePerGas: BigInt(gasEstimate.maxFeePerGas),
|
|
304
|
-
maxPriorityFeePerGas: BigInt(gasEstimate.maxPriorityFeePerGas),
|
|
305
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
306
|
-
signature: "0x",
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Build a UserOperation to ONLY deploy the account (empty callData)
|
|
312
|
-
*/
|
|
313
|
-
async buildDeployUserOperation(): Promise<UserOperation> {
|
|
314
|
-
if (!this.owner || !this.smartAccountAddress) {
|
|
315
|
-
throw new Error("Not connected");
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const isDeployed = await this.isAccountDeployed();
|
|
319
|
-
if (isDeployed) {
|
|
320
|
-
throw new Error("Account is already deployed");
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const initCode = this.buildInitCode();
|
|
324
|
-
const callData = "0x"; // Empty callData for deployment only
|
|
325
|
-
const nonce = await this.getNonce();
|
|
326
|
-
|
|
327
|
-
// Estimate gas
|
|
328
|
-
const gasEstimate = await this.estimateGas({
|
|
329
|
-
sender: this.smartAccountAddress,
|
|
330
|
-
nonce,
|
|
331
|
-
initCode: initCode as Hex,
|
|
332
|
-
callData,
|
|
333
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
return {
|
|
337
|
-
sender: this.smartAccountAddress,
|
|
338
|
-
nonce,
|
|
339
|
-
initCode: initCode as Hex,
|
|
340
|
-
callData,
|
|
341
|
-
callGasLimit: BigInt(gasEstimate.callGasLimit),
|
|
342
|
-
verificationGasLimit: BigInt(gasEstimate.verificationGasLimit),
|
|
343
|
-
preVerificationGas: BigInt(gasEstimate.preVerificationGas),
|
|
344
|
-
maxFeePerGas: BigInt(gasEstimate.maxFeePerGas),
|
|
345
|
-
maxPriorityFeePerGas: BigInt(gasEstimate.maxPriorityFeePerGas),
|
|
346
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
347
|
-
signature: "0x",
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Calculate the UserOperation hash
|
|
353
|
-
*/
|
|
354
|
-
getUserOpHash(userOp: UserOperation): Hex {
|
|
355
|
-
const packed = encodeAbiParameters(
|
|
356
|
-
[
|
|
357
|
-
{ type: "address" },
|
|
358
|
-
{ type: "uint256" },
|
|
359
|
-
{ type: "bytes32" },
|
|
360
|
-
{ type: "bytes32" },
|
|
361
|
-
{ type: "uint256" },
|
|
362
|
-
{ type: "uint256" },
|
|
363
|
-
{ type: "uint256" },
|
|
364
|
-
{ type: "uint256" },
|
|
365
|
-
{ type: "uint256" },
|
|
366
|
-
{ type: "bytes32" },
|
|
367
|
-
],
|
|
368
|
-
[
|
|
369
|
-
userOp.sender,
|
|
370
|
-
userOp.nonce,
|
|
371
|
-
keccak256(userOp.initCode),
|
|
372
|
-
keccak256(userOp.callData),
|
|
373
|
-
userOp.callGasLimit,
|
|
374
|
-
userOp.verificationGasLimit,
|
|
375
|
-
userOp.preVerificationGas,
|
|
376
|
-
userOp.maxFeePerGas,
|
|
377
|
-
userOp.maxPriorityFeePerGas,
|
|
378
|
-
keccak256(userOp.paymasterAndData),
|
|
379
|
-
]
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
const packedHash = keccak256(packed);
|
|
383
|
-
|
|
384
|
-
return keccak256(
|
|
385
|
-
encodeAbiParameters(
|
|
386
|
-
[{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
|
|
387
|
-
[packedHash, this.entryPointAddress, BigInt(this.chainConfig.chain.id)]
|
|
388
|
-
)
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Sign a UserOperation with MetaMask
|
|
394
|
-
*/
|
|
395
|
-
async signUserOperation(userOp: UserOperation): Promise<UserOperation> {
|
|
396
|
-
if (!this.owner) {
|
|
397
|
-
throw new Error("Not connected");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const userOpHash = this.getUserOpHash(userOp);
|
|
401
|
-
|
|
402
|
-
// Sign with MetaMask using personal_sign (EIP-191)
|
|
403
|
-
const signature = (await window.ethereum!.request({
|
|
404
|
-
method: "personal_sign",
|
|
405
|
-
params: [userOpHash, this.owner],
|
|
406
|
-
})) as Hex;
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
...userOp,
|
|
410
|
-
signature,
|
|
411
|
-
};
|
|
162
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
163
|
+
return this.tokenService.getAllowance("USDC", this.owner, this.smartAccountAddress);
|
|
412
164
|
}
|
|
413
165
|
|
|
414
|
-
|
|
415
|
-
* Send a signed UserOperation to the bundler
|
|
416
|
-
*/
|
|
417
|
-
async sendUserOperation(userOp: UserOperation): Promise<Hash> {
|
|
418
|
-
return this.bundlerClient.sendUserOperation(userOp);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Wait for a UserOperation to be confirmed
|
|
423
|
-
*/
|
|
424
|
-
async waitForUserOperation(
|
|
425
|
-
userOpHash: Hash,
|
|
426
|
-
timeout = 60000
|
|
427
|
-
): Promise<UserOpReceipt> {
|
|
428
|
-
return this.bundlerClient.waitForUserOperation(userOpHash, timeout);
|
|
429
|
-
}
|
|
166
|
+
// --- Transactions ---
|
|
430
167
|
|
|
168
|
+
async deployAccount(): Promise<UserOpReceipt> {
|
|
169
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
431
170
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
): Promise<ApprovalSupportResult> {
|
|
440
|
-
if (!this.owner) {
|
|
441
|
-
throw new Error("Not connected");
|
|
171
|
+
try {
|
|
172
|
+
const userOp = await this.userOpBuilder.buildDeployUserOp(this.owner, this.smartAccountAddress);
|
|
173
|
+
const signed = await this.signUserOperation(userOp);
|
|
174
|
+
const hash = await this.sendUserOperation(signed);
|
|
175
|
+
return await this.waitForUserOperation(hash);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw this.decodeError(error);
|
|
442
178
|
}
|
|
443
|
-
return this.bundlerClient.requestApprovalSupport(token, this.owner, spender, amount);
|
|
444
179
|
}
|
|
445
180
|
|
|
446
|
-
/**
|
|
447
|
-
* Deploy the Smart Account
|
|
448
|
-
*/
|
|
449
|
-
async deployAccount(): Promise<UserOpReceipt> {
|
|
450
|
-
const userOp = await this.buildDeployUserOperation();
|
|
451
|
-
const signed = await this.signUserOperation(userOp);
|
|
452
|
-
const hash = await this.sendUserOperation(signed);
|
|
453
|
-
return await this.waitForUserOperation(hash);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Send a single transaction via the Smart Account
|
|
458
|
-
* Abstracts: Build -> Sign -> Send -> Wait
|
|
459
|
-
*/
|
|
460
181
|
async sendTransaction(
|
|
461
182
|
tx: { target: Address; value?: bigint; data?: Hex }
|
|
462
183
|
): Promise<UserOpReceipt> {
|
|
463
184
|
return this.sendBatchTransaction([tx]);
|
|
464
185
|
}
|
|
465
186
|
|
|
466
|
-
/**
|
|
467
|
-
* Send multiple transactions via the Smart Account (Batched)
|
|
468
|
-
* Abstracts: Build -> Sign -> Send -> Wait
|
|
469
|
-
*/
|
|
470
187
|
async sendBatchTransaction(
|
|
471
188
|
txs: { target: Address; value?: bigint; data?: Hex }[]
|
|
472
189
|
): Promise<UserOpReceipt> {
|
|
473
|
-
|
|
190
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
191
|
+
|
|
192
|
+
// Normalize
|
|
474
193
|
const transactions = txs.map(tx => ({
|
|
475
194
|
target: tx.target,
|
|
476
195
|
value: tx.value ?? 0n,
|
|
@@ -478,7 +197,11 @@ export class AccountAbstraction {
|
|
|
478
197
|
}));
|
|
479
198
|
|
|
480
199
|
try {
|
|
481
|
-
const userOp = await this.buildUserOperationBatch(
|
|
200
|
+
const userOp = await this.userOpBuilder.buildUserOperationBatch(
|
|
201
|
+
this.owner,
|
|
202
|
+
this.smartAccountAddress,
|
|
203
|
+
transactions
|
|
204
|
+
);
|
|
482
205
|
const signed = await this.signUserOperation(userOp);
|
|
483
206
|
const hash = await this.sendUserOperation(signed);
|
|
484
207
|
return await this.waitForUserOperation(hash);
|
|
@@ -487,49 +210,47 @@ export class AccountAbstraction {
|
|
|
487
210
|
}
|
|
488
211
|
}
|
|
489
212
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const decoded = decodeErrorResult({
|
|
504
|
-
abi: [
|
|
505
|
-
{
|
|
506
|
-
inputs: [{ name: "message", type: "string" }],
|
|
507
|
-
name: "Error",
|
|
508
|
-
type: "error"
|
|
509
|
-
},
|
|
510
|
-
// Add common EntryPoint errors if known, but generic Reverts are most common
|
|
511
|
-
],
|
|
512
|
-
data: hexMatch[0] as Hex
|
|
513
|
-
});
|
|
213
|
+
async deposit(amount: bigint): Promise<Hash> {
|
|
214
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
215
|
+
|
|
216
|
+
const txHash = await window.ethereum!.request({
|
|
217
|
+
method: "eth_sendTransaction",
|
|
218
|
+
params: [{
|
|
219
|
+
from: this.owner,
|
|
220
|
+
to: this.smartAccountAddress,
|
|
221
|
+
value: "0x" + amount.toString(16)
|
|
222
|
+
}]
|
|
223
|
+
}) as Hash;
|
|
224
|
+
return txHash;
|
|
225
|
+
}
|
|
514
226
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
227
|
+
async transfer(
|
|
228
|
+
token: Address | string,
|
|
229
|
+
recipient: Address,
|
|
230
|
+
amount: bigint
|
|
231
|
+
): Promise<UserOpReceipt> {
|
|
232
|
+
const tokenAddress = this.getTokenAddress(token);
|
|
233
|
+
|
|
234
|
+
// Native Transfer check
|
|
235
|
+
if (tokenAddress === "0x0000000000000000000000000000000000000000") {
|
|
236
|
+
return this.sendTransaction({
|
|
237
|
+
target: recipient,
|
|
238
|
+
value: amount,
|
|
239
|
+
data: "0x"
|
|
240
|
+
});
|
|
521
241
|
}
|
|
522
242
|
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
243
|
+
// ERC-20
|
|
244
|
+
const data = this.tokenService.encodeTransfer(recipient, amount);
|
|
245
|
+
return this.sendTransaction({
|
|
246
|
+
target: tokenAddress,
|
|
247
|
+
value: 0n,
|
|
248
|
+
data
|
|
249
|
+
});
|
|
528
250
|
}
|
|
529
251
|
|
|
530
252
|
/**
|
|
531
|
-
* Approve a token for the Smart Account
|
|
532
|
-
* Checks for gas sponsorship (Relayer funding) if needed.
|
|
253
|
+
* Approve a token for the Smart Account
|
|
533
254
|
*/
|
|
534
255
|
async approveToken(
|
|
535
256
|
token: Address,
|
|
@@ -538,19 +259,10 @@ export class AccountAbstraction {
|
|
|
538
259
|
): Promise<Hash | "NOT_NEEDED"> {
|
|
539
260
|
if (!this.owner) throw new Error("Not connected");
|
|
540
261
|
|
|
541
|
-
// 1. Check if we need funding
|
|
542
262
|
const support = await this.requestApprovalSupport(token, spender, amount);
|
|
543
263
|
|
|
544
264
|
if (support.type === "approve") {
|
|
545
|
-
|
|
546
|
-
const data = encodeFunctionData({
|
|
547
|
-
abi: erc20Abi,
|
|
548
|
-
functionName: "approve",
|
|
549
|
-
args: [spender, amount]
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
// 3. Send transaction via Wallet (MetaMask)
|
|
553
|
-
// If funding was needed, the Relayer has already sent ETH to this.owner
|
|
265
|
+
const data = this.tokenService.encodeApprove(spender, amount);
|
|
554
266
|
const txHash = await window.ethereum!.request({
|
|
555
267
|
method: "eth_sendTransaction",
|
|
556
268
|
params: [{
|
|
@@ -559,46 +271,77 @@ export class AccountAbstraction {
|
|
|
559
271
|
data,
|
|
560
272
|
}]
|
|
561
273
|
}) as Hash;
|
|
562
|
-
|
|
563
274
|
return txHash;
|
|
564
275
|
}
|
|
565
276
|
|
|
566
|
-
if (support.type === "permit")
|
|
567
|
-
throw new Error("Permit not yet supported in this SDK version");
|
|
568
|
-
}
|
|
569
|
-
|
|
277
|
+
if (support.type === "permit") throw new Error("Permit not yet supported");
|
|
570
278
|
return "NOT_NEEDED";
|
|
571
279
|
}
|
|
572
280
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
});
|
|
281
|
+
// --- Core Bridge to Bundler/UserOp ---
|
|
282
|
+
|
|
283
|
+
// Deprecated/Legacy but kept for compatibility or advanced usage?
|
|
284
|
+
// buildUserOperationBatch moved to internal usage mostly, but maybe exposed?
|
|
285
|
+
// If I remove them from public API, that is a BREAKING change if user used them.
|
|
286
|
+
// User requested "modularize", but usually expects same public API.
|
|
287
|
+
// I will expose them as simple delegates if needed, or assume they primarily use sendBatchTransaction.
|
|
288
|
+
// The previous implementation exposed `buildUserOperationBatch`.
|
|
289
|
+
async buildUserOperationBatch(transactions: any[]) {
|
|
290
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
291
|
+
return this.userOpBuilder.buildUserOperationBatch(this.owner, this.smartAccountAddress, transactions);
|
|
292
|
+
}
|
|
586
293
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
294
|
+
async signUserOperation(userOp: UserOperation): Promise<UserOperation> {
|
|
295
|
+
if (!this.owner) throw new Error("Not connected");
|
|
296
|
+
|
|
297
|
+
const userOpHash = this.userOpBuilder.getUserOpHash(userOp);
|
|
298
|
+
|
|
299
|
+
const signature = (await window.ethereum!.request({
|
|
300
|
+
method: "personal_sign",
|
|
301
|
+
params: [userOpHash, this.owner],
|
|
302
|
+
})) as Hex;
|
|
303
|
+
|
|
304
|
+
return { ...userOp, signature };
|
|
592
305
|
}
|
|
593
306
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return this.owner;
|
|
307
|
+
async sendUserOperation(userOp: UserOperation): Promise<Hash> {
|
|
308
|
+
return this.bundlerClient.sendUserOperation(userOp);
|
|
597
309
|
}
|
|
598
310
|
|
|
599
|
-
|
|
600
|
-
return this.
|
|
311
|
+
async waitForUserOperation(hash: Hash, timeout = 60000) {
|
|
312
|
+
return this.bundlerClient.waitForUserOperation(hash, timeout);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Internal but exposed via BundlerClient originally
|
|
316
|
+
async requestApprovalSupport(token: Address, spender: Address, amount: bigint): Promise<ApprovalSupportResult> {
|
|
317
|
+
if (!this.owner) throw new Error("Not connected");
|
|
318
|
+
return this.bundlerClient.requestApprovalSupport(token, this.owner, spender, amount);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Error Decoding (Private)
|
|
322
|
+
private decodeError(error: any): Error {
|
|
323
|
+
const msg = error?.message || "";
|
|
324
|
+
const hexMatch = msg.match(/(0x[0-9a-fA-F]+)/);
|
|
325
|
+
|
|
326
|
+
if (hexMatch) {
|
|
327
|
+
try {
|
|
328
|
+
const decoded = decodeErrorResult({
|
|
329
|
+
abi: [{ inputs: [{ name: "message", type: "string" }], name: "Error", type: "error" }],
|
|
330
|
+
data: hexMatch[0] as Hex
|
|
331
|
+
});
|
|
332
|
+
if (decoded.errorName === "Error") return new Error(`Smart Account Error: ${decoded.args[0]}`);
|
|
333
|
+
} catch (e) { /* ignore */ }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (msg.includes("AA21")) return new Error("Smart Account: Native transfer failed (ETH missing?)");
|
|
337
|
+
if (msg.includes("AA25")) return new Error("Smart Account: Invalid account nonce");
|
|
338
|
+
|
|
339
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
601
340
|
}
|
|
341
|
+
|
|
342
|
+
// Getters
|
|
343
|
+
getOwner() { return this.owner; }
|
|
344
|
+
getSmartAccount() { return this.smartAccountAddress; }
|
|
602
345
|
}
|
|
603
346
|
|
|
604
347
|
// Global window types for MetaMask
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type Address, type PublicClient, encodeFunctionData } from "viem";
|
|
2
|
+
import { type ChainConfig, type Token } from "./types";
|
|
3
|
+
import { erc20Abi } from "./constants";
|
|
4
|
+
|
|
5
|
+
export class TokenService {
|
|
6
|
+
private tokens: Map<string, Token> = new Map();
|
|
7
|
+
private publicClient: PublicClient;
|
|
8
|
+
|
|
9
|
+
constructor(chainConfig: ChainConfig, publicClient: PublicClient) {
|
|
10
|
+
this.publicClient = publicClient;
|
|
11
|
+
|
|
12
|
+
// Initialize Tokens
|
|
13
|
+
chainConfig.tokens.forEach(token => {
|
|
14
|
+
this.tokens.set(token.symbol.toUpperCase(), token);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve token address from symbol or return address if provided
|
|
20
|
+
*/
|
|
21
|
+
getTokenAddress(token: string | Address): Address {
|
|
22
|
+
// Native Token (ETH)
|
|
23
|
+
if (token === "ETH") {
|
|
24
|
+
return "0x0000000000000000000000000000000000000000";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (token.startsWith("0x")) return token as Address;
|
|
28
|
+
const info = this.tokens.get(token.toUpperCase());
|
|
29
|
+
if (!info) throw new Error(`Token ${token} not found in chain config`);
|
|
30
|
+
return info.address;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get balance of a token for an account
|
|
35
|
+
*/
|
|
36
|
+
async getBalance(token: string | Address, account: Address): Promise<bigint> {
|
|
37
|
+
const address = this.getTokenAddress(token);
|
|
38
|
+
|
|
39
|
+
// Native Balance
|
|
40
|
+
if (address === "0x0000000000000000000000000000000000000000") {
|
|
41
|
+
return await this.publicClient.getBalance({ address: account });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ERC-20 Balance
|
|
45
|
+
return await this.publicClient.readContract({
|
|
46
|
+
address,
|
|
47
|
+
abi: erc20Abi,
|
|
48
|
+
functionName: "balanceOf",
|
|
49
|
+
args: [account],
|
|
50
|
+
}) as bigint;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get allowance (ERC-20 only)
|
|
55
|
+
*/
|
|
56
|
+
async getAllowance(token: string | Address, owner: Address, spender: Address): Promise<bigint> {
|
|
57
|
+
const address = this.getTokenAddress(token);
|
|
58
|
+
|
|
59
|
+
if (address === "0x0000000000000000000000000000000000000000") {
|
|
60
|
+
return 0n; // Native token has no allowance
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return await this.publicClient.readContract({
|
|
64
|
+
address,
|
|
65
|
+
abi: erc20Abi,
|
|
66
|
+
functionName: "allowance",
|
|
67
|
+
args: [owner, spender],
|
|
68
|
+
}) as bigint;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Encode transfer data
|
|
73
|
+
*/
|
|
74
|
+
encodeTransfer(recipient: Address, amount: bigint): `0x${string}` {
|
|
75
|
+
return encodeFunctionData({
|
|
76
|
+
abi: erc20Abi,
|
|
77
|
+
functionName: "transfer",
|
|
78
|
+
args: [recipient, amount]
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Encode approve data
|
|
84
|
+
*/
|
|
85
|
+
encodeApprove(spender: Address, amount: bigint): `0x${string}` {
|
|
86
|
+
return encodeFunctionData({
|
|
87
|
+
abi: erc20Abi,
|
|
88
|
+
functionName: "approve",
|
|
89
|
+
args: [spender, amount]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Address,
|
|
3
|
+
type Hash,
|
|
4
|
+
type Hex,
|
|
5
|
+
type PublicClient,
|
|
6
|
+
encodeFunctionData,
|
|
7
|
+
encodeAbiParameters,
|
|
8
|
+
keccak256
|
|
9
|
+
} from "viem";
|
|
10
|
+
import { type ChainConfig, type UserOperation, type GasEstimate } from "./types";
|
|
11
|
+
import { BundlerClient } from "./BundlerClient";
|
|
12
|
+
import { factoryAbi, smartAccountAbi, entryPointAbi } from "./constants";
|
|
13
|
+
|
|
14
|
+
export class UserOpBuilder {
|
|
15
|
+
private chainConfig: ChainConfig;
|
|
16
|
+
private bundlerClient: BundlerClient;
|
|
17
|
+
private publicClient: PublicClient;
|
|
18
|
+
private entryPointAddress: Address;
|
|
19
|
+
private factoryAddress: Address;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
chainConfig: ChainConfig,
|
|
23
|
+
bundlerClient: BundlerClient,
|
|
24
|
+
publicClient: PublicClient
|
|
25
|
+
) {
|
|
26
|
+
this.chainConfig = chainConfig;
|
|
27
|
+
this.bundlerClient = bundlerClient;
|
|
28
|
+
this.publicClient = publicClient;
|
|
29
|
+
|
|
30
|
+
// Resolved in AA or here? Let's assume passed valid config or resolve again
|
|
31
|
+
// Ideally we shouldn't duplicate logic. AA resolves them.
|
|
32
|
+
// Let's rely on config having them or resolving valid ones.
|
|
33
|
+
// For now take from config or defaults.
|
|
34
|
+
this.entryPointAddress = chainConfig.entryPointAddress!; // Assumed validated by AA
|
|
35
|
+
this.factoryAddress = chainConfig.factoryAddress!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getNonce(smartAccountAddress: Address): Promise<bigint> {
|
|
39
|
+
return await this.publicClient.readContract({
|
|
40
|
+
address: this.entryPointAddress,
|
|
41
|
+
abi: entryPointAbi,
|
|
42
|
+
functionName: "getNonce",
|
|
43
|
+
args: [smartAccountAddress, 0n],
|
|
44
|
+
}) as bigint;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
buildInitCode(owner: Address): Hex {
|
|
48
|
+
const createAccountData = encodeFunctionData({
|
|
49
|
+
abi: factoryAbi,
|
|
50
|
+
functionName: "createAccount",
|
|
51
|
+
args: [owner, 0n],
|
|
52
|
+
});
|
|
53
|
+
return `${this.factoryAddress}${createAccountData.slice(2)}` as Hex;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async isAccountDeployed(smartAccountAddress: Address): Promise<boolean> {
|
|
57
|
+
const code = await this.publicClient.getCode({
|
|
58
|
+
address: smartAccountAddress,
|
|
59
|
+
});
|
|
60
|
+
return code !== undefined && code !== "0x";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async buildUserOperationBatch(
|
|
64
|
+
owner: Address,
|
|
65
|
+
smartAccountAddress: Address,
|
|
66
|
+
transactions: { target: Address; value: bigint; data: Hex }[]
|
|
67
|
+
): Promise<UserOperation> {
|
|
68
|
+
const isDeployed = await this.isAccountDeployed(smartAccountAddress);
|
|
69
|
+
const initCode = isDeployed ? "0x" : this.buildInitCode(owner);
|
|
70
|
+
|
|
71
|
+
const targets = transactions.map((tx) => tx.target);
|
|
72
|
+
const values = transactions.map((tx) => tx.value);
|
|
73
|
+
const datas = transactions.map((tx) => tx.data);
|
|
74
|
+
|
|
75
|
+
const callData = encodeFunctionData({
|
|
76
|
+
abi: smartAccountAbi,
|
|
77
|
+
functionName: "executeBatch",
|
|
78
|
+
args: [targets, values, datas],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const nonce = await this.getNonce(smartAccountAddress);
|
|
82
|
+
|
|
83
|
+
const partialOp = {
|
|
84
|
+
sender: smartAccountAddress,
|
|
85
|
+
nonce,
|
|
86
|
+
initCode: initCode as Hex,
|
|
87
|
+
callData,
|
|
88
|
+
paymasterAndData: (this.chainConfig.paymasterAddress || "0x") as Hex,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const gasEstimate = await this.bundlerClient.estimateGas(partialOp);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...partialOp,
|
|
95
|
+
callGasLimit: BigInt(gasEstimate.callGasLimit),
|
|
96
|
+
verificationGasLimit: BigInt(gasEstimate.verificationGasLimit),
|
|
97
|
+
preVerificationGas: BigInt(gasEstimate.preVerificationGas),
|
|
98
|
+
maxFeePerGas: BigInt(gasEstimate.maxFeePerGas),
|
|
99
|
+
maxPriorityFeePerGas: BigInt(gasEstimate.maxPriorityFeePerGas),
|
|
100
|
+
signature: "0x",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async buildDeployUserOp(
|
|
105
|
+
owner: Address,
|
|
106
|
+
smartAccountAddress: Address
|
|
107
|
+
): Promise<UserOperation> {
|
|
108
|
+
const isDeployed = await this.isAccountDeployed(smartAccountAddress);
|
|
109
|
+
if (isDeployed) throw new Error("Account already deployed");
|
|
110
|
+
|
|
111
|
+
const initCode = this.buildInitCode(owner);
|
|
112
|
+
const callData = "0x";
|
|
113
|
+
const nonce = await this.getNonce(smartAccountAddress);
|
|
114
|
+
|
|
115
|
+
const partialOp = {
|
|
116
|
+
sender: smartAccountAddress,
|
|
117
|
+
nonce,
|
|
118
|
+
initCode: initCode as Hex,
|
|
119
|
+
callData: callData as Hex,
|
|
120
|
+
paymasterAndData: (this.chainConfig.paymasterAddress || "0x") as Hex,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const gasEstimate = await this.bundlerClient.estimateGas(partialOp);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
...partialOp,
|
|
127
|
+
callGasLimit: BigInt(gasEstimate.callGasLimit),
|
|
128
|
+
verificationGasLimit: BigInt(gasEstimate.verificationGasLimit),
|
|
129
|
+
preVerificationGas: BigInt(gasEstimate.preVerificationGas),
|
|
130
|
+
maxFeePerGas: BigInt(gasEstimate.maxFeePerGas),
|
|
131
|
+
maxPriorityFeePerGas: BigInt(gasEstimate.maxPriorityFeePerGas),
|
|
132
|
+
signature: "0x",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getUserOpHash(userOp: UserOperation): Hex {
|
|
137
|
+
const packed = encodeAbiParameters(
|
|
138
|
+
[
|
|
139
|
+
{ type: "address" },
|
|
140
|
+
{ type: "uint256" },
|
|
141
|
+
{ type: "bytes32" },
|
|
142
|
+
{ type: "bytes32" },
|
|
143
|
+
{ type: "uint256" },
|
|
144
|
+
{ type: "uint256" },
|
|
145
|
+
{ type: "uint256" },
|
|
146
|
+
{ type: "uint256" },
|
|
147
|
+
{ type: "uint256" },
|
|
148
|
+
{ type: "bytes32" },
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
userOp.sender,
|
|
152
|
+
userOp.nonce,
|
|
153
|
+
keccak256(userOp.initCode),
|
|
154
|
+
keccak256(userOp.callData),
|
|
155
|
+
userOp.callGasLimit,
|
|
156
|
+
userOp.verificationGasLimit,
|
|
157
|
+
userOp.preVerificationGas,
|
|
158
|
+
userOp.maxFeePerGas,
|
|
159
|
+
userOp.maxPriorityFeePerGas,
|
|
160
|
+
keccak256(userOp.paymasterAndData),
|
|
161
|
+
]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const packedHash = keccak256(packed);
|
|
165
|
+
|
|
166
|
+
return keccak256(
|
|
167
|
+
encodeAbiParameters(
|
|
168
|
+
[{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
|
|
169
|
+
[packedHash, this.entryPointAddress, BigInt(this.chainConfig.chain.id)]
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/chains.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ChainConfig } from "./types";
|
|
2
|
+
import { base, baseSepolia } from "viem/chains";
|
|
3
|
+
|
|
4
|
+
export const BASE_MAINNET: ChainConfig = {
|
|
5
|
+
chain: base,
|
|
6
|
+
bundlerUrl: "http://localhost:3000/rpc?chain=base", // Default to local bundler pattern
|
|
7
|
+
|
|
8
|
+
// Addresses
|
|
9
|
+
entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
|
|
10
|
+
factoryAddress: "0xe2584152891E4769025807DEa0cD611F135aDC68",
|
|
11
|
+
paymasterAddress: "0x1e13Eb16C565E3f3FDe49A011755e50410bb1F95",
|
|
12
|
+
|
|
13
|
+
tokens: [
|
|
14
|
+
{
|
|
15
|
+
symbol: "USDC",
|
|
16
|
+
decimals: 6,
|
|
17
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
symbol: "ETH",
|
|
21
|
+
decimals: 18,
|
|
22
|
+
address: "0x0000000000000000000000000000000000000000"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const BASE_SEPOLIA: ChainConfig = {
|
|
28
|
+
chain: baseSepolia,
|
|
29
|
+
bundlerUrl: "http://localhost:3000/rpc?chain=baseSepolia", // Default to local bundler pattern
|
|
30
|
+
|
|
31
|
+
// Addresses
|
|
32
|
+
entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
|
|
33
|
+
factoryAddress: "0x9406Cc6185a346906296840746125a0E44976454",
|
|
34
|
+
// Paymaster not configured in deployments.ts for Sepolia?
|
|
35
|
+
|
|
36
|
+
tokens: [
|
|
37
|
+
{
|
|
38
|
+
symbol: "USDC",
|
|
39
|
+
decimals: 6,
|
|
40
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
symbol: "ETH",
|
|
44
|
+
decimals: 18,
|
|
45
|
+
address: "0x0000000000000000000000000000000000000000"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Map accessible by ChainID
|
|
51
|
+
export const CHAIN_CONFIGS: Record<number, ChainConfig> = {
|
|
52
|
+
[base.id]: BASE_MAINNET,
|
|
53
|
+
[baseSepolia.id]: BASE_SEPOLIA
|
|
54
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
|
|
1
|
+
// Core
|
|
2
|
+
export { AccountAbstraction } from "./AccountAbstraction";
|
|
3
|
+
export { BundlerClient } from "./BundlerClient";
|
|
4
|
+
|
|
5
|
+
// Config & Registry
|
|
6
|
+
export { BASE_MAINNET, BASE_SEPOLIA, CHAIN_CONFIGS } from "./chains";
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type { ChainConfig, Token, UserOperation, UserOpReceipt } from "./types";
|
|
10
|
+
|
|
11
|
+
// Constants (ABIs)
|
|
12
|
+
export { erc20Abi, smartAccountAbi, entryPointAbi } from "./constants";
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type Address, type Chain, type Hash, type Hex } from "viem";
|
|
2
2
|
|
|
3
|
+
export interface Token {
|
|
4
|
+
symbol: string;
|
|
5
|
+
decimals: number;
|
|
6
|
+
address: Address;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export interface ChainConfig {
|
|
4
10
|
chain: Chain;
|
|
5
11
|
rpcUrl?: string; // Optional, defaults to chain.rpcUrls.default
|
|
@@ -7,7 +13,7 @@ export interface ChainConfig {
|
|
|
7
13
|
entryPointAddress?: Address;
|
|
8
14
|
factoryAddress?: Address;
|
|
9
15
|
paymasterAddress?: Address;
|
|
10
|
-
|
|
16
|
+
tokens: Token[];
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export interface UserOperation {
|
package/src/deployments.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type Address } from "viem";
|
|
2
|
-
|
|
3
|
-
export const DEPLOYMENTS: Record<number, {
|
|
4
|
-
entryPoint: Address;
|
|
5
|
-
factory: Address;
|
|
6
|
-
paymaster?: Address;
|
|
7
|
-
usdc: Address;
|
|
8
|
-
}> = {
|
|
9
|
-
// Base Mainnet
|
|
10
|
-
8453: {
|
|
11
|
-
entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
|
|
12
|
-
factory: "0xe2584152891E4769025807DEa0cD611F135aDC68",
|
|
13
|
-
paymaster: "0x1e13Eb16C565E3f3FDe49A011755e50410bb1F95",
|
|
14
|
-
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
15
|
-
},
|
|
16
|
-
// Base Sepolia
|
|
17
|
-
84532: {
|
|
18
|
-
entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
|
|
19
|
-
factory: "0x9406Cc6185a346906296840746125a0E44976454",
|
|
20
|
-
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
21
|
-
}
|
|
22
|
-
};
|