@1llet.xyz/erc4337-gasless-sdk 0.1.7 → 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 +58 -14
- package/package.json +1 -1
- package/src/AccountAbstraction.ts +167 -328
- 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
|
|
@@ -143,6 +137,56 @@ const address = await aa.getSmartAccountAddress(ownerAddress);
|
|
|
143
137
|
const receipt = await aa.deployAccount();
|
|
144
138
|
```
|
|
145
139
|
|
|
140
|
+
### Simplified Transactions (v0.2.0+)
|
|
141
|
+
|
|
142
|
+
Send transactions without manually building, signing, and waiting.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// 1. Send ETH or Call Contract (Single)
|
|
146
|
+
const receipt = await aa.sendTransaction({
|
|
147
|
+
target: "0x123...",
|
|
148
|
+
value: 1000000000000000000n, // 1 ETH
|
|
149
|
+
data: "0x..." // Optional callData
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 2. Send Multiple Transactions (Batch)
|
|
153
|
+
// Great for approving + swapping, or multiple transfers
|
|
154
|
+
const receipt = await aa.sendBatchTransaction([
|
|
155
|
+
{ target: "0xToken...", data: encodeApproveData },
|
|
156
|
+
{ target: "0xSwap...", data: encodeSwapData }
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// 3. Transfer ERC-20 Tokens (Helper)
|
|
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
|
+
```
|
|
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
|
+
|
|
185
|
+
### Error Decoding
|
|
186
|
+
The SDK now automatically tries to decode cryptic "0x..." errors from the EntryPoint into readable messages like:
|
|
187
|
+
- `Smart Account Error: Transfer amount exceeds balance`
|
|
188
|
+
- `Smart Account: Native transfer failed`
|
|
189
|
+
|
|
146
190
|
### Simplified Approvals
|
|
147
191
|
|
|
148
192
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
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,
|
|
10
|
-
type PublicClient
|
|
7
|
+
type PublicClient,
|
|
8
|
+
decodeErrorResult
|
|
11
9
|
} from "viem";
|
|
12
10
|
import {
|
|
13
11
|
factoryAbi,
|
|
14
|
-
entryPointAbi,
|
|
15
|
-
smartAccountAbi,
|
|
16
|
-
erc20Abi,
|
|
17
12
|
} from "./constants";
|
|
18
13
|
import {
|
|
19
14
|
type ChainConfig,
|
|
20
15
|
type UserOperation,
|
|
21
|
-
type GasEstimate,
|
|
22
16
|
type UserOpReceipt,
|
|
23
|
-
type ApprovalSupportResult
|
|
17
|
+
type ApprovalSupportResult,
|
|
18
|
+
type Token
|
|
24
19
|
} from "./types";
|
|
25
|
-
import { DEPLOYMENTS } from "./deployments";
|
|
26
20
|
import { BundlerClient } from "./BundlerClient";
|
|
21
|
+
import { TokenService } from "./TokenService";
|
|
22
|
+
import { UserOpBuilder } from "./UserOpBuilder";
|
|
27
23
|
|
|
28
24
|
/**
|
|
29
25
|
* ERC-4337 Account Abstraction Client
|
|
@@ -35,41 +31,35 @@ export class AccountAbstraction {
|
|
|
35
31
|
private publicClient: PublicClient;
|
|
36
32
|
private bundlerClient: BundlerClient;
|
|
37
33
|
|
|
34
|
+
// Services
|
|
35
|
+
private tokenService: TokenService;
|
|
36
|
+
private userOpBuilder: UserOpBuilder;
|
|
37
|
+
|
|
38
38
|
// Resolved addresses
|
|
39
39
|
private entryPointAddress: Address;
|
|
40
40
|
private factoryAddress: Address;
|
|
41
|
-
private paymasterAddress?: Address;
|
|
42
|
-
private usdcAddress: Address;
|
|
43
41
|
|
|
44
42
|
constructor(chainConfig: ChainConfig) {
|
|
45
43
|
this.chainConfig = chainConfig;
|
|
46
|
-
const chainId = chainConfig.chain.id;
|
|
47
|
-
const defaults = DEPLOYMENTS[chainId];
|
|
48
|
-
|
|
49
|
-
// Resolve addresses (Config > Defaults > Error)
|
|
50
|
-
const entryPoint = chainConfig.entryPointAddress || defaults?.entryPoint;
|
|
51
|
-
if (!entryPoint) throw new Error(`EntryPoint address not found for chain ${chainId}`);
|
|
52
|
-
this.entryPointAddress = entryPoint;
|
|
53
|
-
|
|
54
|
-
const factory = chainConfig.factoryAddress || defaults?.factory;
|
|
55
|
-
if (!factory) throw new Error(`Factory address not found for chain ${chainId}`);
|
|
56
|
-
this.factoryAddress = factory;
|
|
57
44
|
|
|
58
|
-
|
|
59
|
-
if (!
|
|
60
|
-
this.
|
|
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;
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Use provided RPC or default from chain
|
|
51
|
+
// Setup Clients
|
|
65
52
|
const rpcUrl = chainConfig.rpcUrl || chainConfig.chain.rpcUrls.default.http[0];
|
|
66
|
-
|
|
67
53
|
this.publicClient = createPublicClient({
|
|
68
54
|
chain: chainConfig.chain,
|
|
69
55
|
transport: http(rpcUrl),
|
|
70
56
|
});
|
|
71
57
|
|
|
72
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);
|
|
73
63
|
}
|
|
74
64
|
|
|
75
65
|
/**
|
|
@@ -85,19 +75,15 @@ export class AccountAbstraction {
|
|
|
85
75
|
method: "eth_requestAccounts",
|
|
86
76
|
})) as string[];
|
|
87
77
|
|
|
88
|
-
if (!accounts || accounts.length === 0)
|
|
89
|
-
throw new Error("No accounts found");
|
|
90
|
-
}
|
|
78
|
+
if (!accounts || accounts.length === 0) throw new Error("No accounts found");
|
|
91
79
|
|
|
92
80
|
// Check network
|
|
93
81
|
const chainId = (await window.ethereum.request({
|
|
94
82
|
method: "eth_chainId",
|
|
95
83
|
})) as string;
|
|
96
|
-
|
|
97
84
|
const targetChainId = this.chainConfig.chain.id;
|
|
98
85
|
|
|
99
86
|
if (parseInt(chainId, 16) !== targetChainId) {
|
|
100
|
-
// Switch to configured chain
|
|
101
87
|
try {
|
|
102
88
|
await window.ethereum.request({
|
|
103
89
|
method: "wallet_switchEthereumChain",
|
|
@@ -105,7 +91,6 @@ export class AccountAbstraction {
|
|
|
105
91
|
});
|
|
106
92
|
} catch (switchError: unknown) {
|
|
107
93
|
const error = switchError as { code?: number };
|
|
108
|
-
// Chain not added, add it
|
|
109
94
|
if (error.code === 4902) {
|
|
110
95
|
await window.ethereum.request({
|
|
111
96
|
method: "wallet_addEthereumChain",
|
|
@@ -114,7 +99,7 @@ export class AccountAbstraction {
|
|
|
114
99
|
chainId: "0x" + targetChainId.toString(16),
|
|
115
100
|
chainName: this.chainConfig.chain.name,
|
|
116
101
|
nativeCurrency: this.chainConfig.chain.nativeCurrency,
|
|
117
|
-
rpcUrls: [this.chainConfig.rpcUrl],
|
|
102
|
+
rpcUrls: [this.chainConfig.rpcUrl || this.chainConfig.chain.rpcUrls.default.http[0]],
|
|
118
103
|
blockExplorerUrls: this.chainConfig.chain.blockExplorers?.default?.url
|
|
119
104
|
? [this.chainConfig.chain.blockExplorers.default.url]
|
|
120
105
|
: [],
|
|
@@ -137,14 +122,14 @@ export class AccountAbstraction {
|
|
|
137
122
|
}
|
|
138
123
|
|
|
139
124
|
/**
|
|
140
|
-
* Get the Smart Account address for an owner
|
|
125
|
+
* Get the Smart Account address for an owner
|
|
141
126
|
*/
|
|
142
127
|
async getSmartAccountAddress(owner: Address): Promise<Address> {
|
|
143
128
|
const address = await this.publicClient.readContract({
|
|
144
129
|
address: this.factoryAddress,
|
|
145
130
|
abi: factoryAbi,
|
|
146
131
|
functionName: "getAccountAddress",
|
|
147
|
-
args: [owner, 0n],
|
|
132
|
+
args: [owner, 0n],
|
|
148
133
|
}) as Address;
|
|
149
134
|
return address;
|
|
150
135
|
}
|
|
@@ -153,356 +138,210 @@ export class AccountAbstraction {
|
|
|
153
138
|
* Check if the Smart Account is deployed
|
|
154
139
|
*/
|
|
155
140
|
async isAccountDeployed(): Promise<boolean> {
|
|
156
|
-
if (!this.smartAccountAddress)
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const code = await this.publicClient.getCode({
|
|
161
|
-
address: this.smartAccountAddress,
|
|
162
|
-
});
|
|
163
|
-
return code !== undefined && code !== "0x";
|
|
141
|
+
if (!this.smartAccountAddress) throw new Error("Not connected");
|
|
142
|
+
return this.userOpBuilder.isAccountDeployed(this.smartAccountAddress);
|
|
164
143
|
}
|
|
165
144
|
|
|
145
|
+
// --- Token Methods (Delegated) ---
|
|
166
146
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
*/
|
|
170
|
-
async getUsdcBalance(): Promise<bigint> {
|
|
171
|
-
if (!this.smartAccountAddress) {
|
|
172
|
-
throw new Error("Not connected");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return await this.publicClient.readContract({
|
|
176
|
-
address: this.usdcAddress,
|
|
177
|
-
abi: erc20Abi,
|
|
178
|
-
functionName: "balanceOf",
|
|
179
|
-
args: [this.smartAccountAddress],
|
|
180
|
-
}) as bigint;
|
|
147
|
+
getTokenAddress(token: string | Address): Address {
|
|
148
|
+
return this.tokenService.getTokenAddress(token);
|
|
181
149
|
}
|
|
182
150
|
|
|
151
|
+
async getUsdcBalance(): Promise<bigint> {
|
|
152
|
+
if (!this.smartAccountAddress) throw new Error("Not connected");
|
|
153
|
+
return this.tokenService.getBalance("USDC", this.smartAccountAddress);
|
|
154
|
+
}
|
|
183
155
|
|
|
184
|
-
/**
|
|
185
|
-
* Get the EOA's USDC balance
|
|
186
|
-
*/
|
|
187
156
|
async getEoaUsdcBalance(): Promise<bigint> {
|
|
188
|
-
if (!this.owner)
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return await this.publicClient.readContract({
|
|
193
|
-
address: this.usdcAddress,
|
|
194
|
-
abi: erc20Abi,
|
|
195
|
-
functionName: "balanceOf",
|
|
196
|
-
args: [this.owner],
|
|
197
|
-
}) as bigint;
|
|
157
|
+
if (!this.owner) throw new Error("Not connected");
|
|
158
|
+
return this.tokenService.getBalance("USDC", this.owner);
|
|
198
159
|
}
|
|
199
160
|
|
|
200
|
-
/**
|
|
201
|
-
* Get the allowance of the Smart Account to spend the EOA's USDC
|
|
202
|
-
*/
|
|
203
161
|
async getAllowance(): Promise<bigint> {
|
|
204
|
-
if (!this.owner || !this.smartAccountAddress)
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return await this.publicClient.readContract({
|
|
209
|
-
address: this.usdcAddress,
|
|
210
|
-
abi: erc20Abi,
|
|
211
|
-
functionName: "allowance",
|
|
212
|
-
args: [this.owner, this.smartAccountAddress],
|
|
213
|
-
}) as bigint;
|
|
162
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
163
|
+
return this.tokenService.getAllowance("USDC", this.owner, this.smartAccountAddress);
|
|
214
164
|
}
|
|
215
165
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
166
|
+
// --- Transactions ---
|
|
167
|
+
|
|
168
|
+
async deployAccount(): Promise<UserOpReceipt> {
|
|
169
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
170
|
+
|
|
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);
|
|
222
178
|
}
|
|
179
|
+
}
|
|
223
180
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
args: [this.smartAccountAddress, 0n],
|
|
229
|
-
}) as bigint;
|
|
181
|
+
async sendTransaction(
|
|
182
|
+
tx: { target: Address; value?: bigint; data?: Hex }
|
|
183
|
+
): Promise<UserOpReceipt> {
|
|
184
|
+
return this.sendBatchTransaction([tx]);
|
|
230
185
|
}
|
|
231
186
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
187
|
+
async sendBatchTransaction(
|
|
188
|
+
txs: { target: Address; value?: bigint; data?: Hex }[]
|
|
189
|
+
): Promise<UserOpReceipt> {
|
|
190
|
+
if (!this.owner || !this.smartAccountAddress) throw new Error("Not connected");
|
|
191
|
+
|
|
192
|
+
// Normalize
|
|
193
|
+
const transactions = txs.map(tx => ({
|
|
194
|
+
target: tx.target,
|
|
195
|
+
value: tx.value ?? 0n,
|
|
196
|
+
data: tx.data ?? "0x"
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const userOp = await this.userOpBuilder.buildUserOperationBatch(
|
|
201
|
+
this.owner,
|
|
202
|
+
this.smartAccountAddress,
|
|
203
|
+
transactions
|
|
204
|
+
);
|
|
205
|
+
const signed = await this.signUserOperation(userOp);
|
|
206
|
+
const hash = await this.sendUserOperation(signed);
|
|
207
|
+
return await this.waitForUserOperation(hash);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw this.decodeError(error);
|
|
238
210
|
}
|
|
239
|
-
|
|
240
|
-
const createAccountData = encodeFunctionData({
|
|
241
|
-
abi: factoryAbi,
|
|
242
|
-
functionName: "createAccount",
|
|
243
|
-
args: [this.owner, 0n],
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
return `${this.factoryAddress}${createAccountData.slice(2)}` as Hex;
|
|
247
211
|
}
|
|
248
212
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
225
|
}
|
|
256
226
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
});
|
|
266
241
|
}
|
|
267
242
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const datas = transactions.map((tx) => tx.data);
|
|
275
|
-
|
|
276
|
-
// Encode callData for executeBatch
|
|
277
|
-
const callData = encodeFunctionData({
|
|
278
|
-
abi: smartAccountAbi,
|
|
279
|
-
functionName: "executeBatch",
|
|
280
|
-
args: [targets, values, datas],
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const nonce = await this.getNonce();
|
|
284
|
-
|
|
285
|
-
// Estimate gas
|
|
286
|
-
const gasEstimate = await this.estimateGas({
|
|
287
|
-
sender: this.smartAccountAddress,
|
|
288
|
-
nonce,
|
|
289
|
-
initCode: initCode as Hex,
|
|
290
|
-
callData,
|
|
291
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
243
|
+
// ERC-20
|
|
244
|
+
const data = this.tokenService.encodeTransfer(recipient, amount);
|
|
245
|
+
return this.sendTransaction({
|
|
246
|
+
target: tokenAddress,
|
|
247
|
+
value: 0n,
|
|
248
|
+
data
|
|
292
249
|
});
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
sender: this.smartAccountAddress,
|
|
296
|
-
nonce,
|
|
297
|
-
initCode: initCode as Hex,
|
|
298
|
-
callData,
|
|
299
|
-
callGasLimit: BigInt(gasEstimate.callGasLimit),
|
|
300
|
-
verificationGasLimit: BigInt(gasEstimate.verificationGasLimit),
|
|
301
|
-
preVerificationGas: BigInt(gasEstimate.preVerificationGas),
|
|
302
|
-
maxFeePerGas: BigInt(gasEstimate.maxFeePerGas),
|
|
303
|
-
maxPriorityFeePerGas: BigInt(gasEstimate.maxPriorityFeePerGas),
|
|
304
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
305
|
-
signature: "0x",
|
|
306
|
-
};
|
|
307
250
|
}
|
|
308
251
|
|
|
309
252
|
/**
|
|
310
|
-
*
|
|
253
|
+
* Approve a token for the Smart Account
|
|
311
254
|
*/
|
|
312
|
-
async
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
255
|
+
async approveToken(
|
|
256
|
+
token: Address,
|
|
257
|
+
spender: Address,
|
|
258
|
+
amount: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129639935n // maxUint256
|
|
259
|
+
): Promise<Hash | "NOT_NEEDED"> {
|
|
260
|
+
if (!this.owner) throw new Error("Not connected");
|
|
316
261
|
|
|
317
|
-
const
|
|
318
|
-
if (isDeployed) {
|
|
319
|
-
throw new Error("Account is already deployed");
|
|
320
|
-
}
|
|
262
|
+
const support = await this.requestApprovalSupport(token, spender, amount);
|
|
321
263
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
264
|
+
if (support.type === "approve") {
|
|
265
|
+
const data = this.tokenService.encodeApprove(spender, amount);
|
|
266
|
+
const txHash = await window.ethereum!.request({
|
|
267
|
+
method: "eth_sendTransaction",
|
|
268
|
+
params: [{
|
|
269
|
+
from: this.owner,
|
|
270
|
+
to: token,
|
|
271
|
+
data,
|
|
272
|
+
}]
|
|
273
|
+
}) as Hash;
|
|
274
|
+
return txHash;
|
|
275
|
+
}
|
|
334
276
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
nonce,
|
|
338
|
-
initCode: initCode as Hex,
|
|
339
|
-
callData,
|
|
340
|
-
callGasLimit: BigInt(gasEstimate.callGasLimit),
|
|
341
|
-
verificationGasLimit: BigInt(gasEstimate.verificationGasLimit),
|
|
342
|
-
preVerificationGas: BigInt(gasEstimate.preVerificationGas),
|
|
343
|
-
maxFeePerGas: BigInt(gasEstimate.maxFeePerGas),
|
|
344
|
-
maxPriorityFeePerGas: BigInt(gasEstimate.maxPriorityFeePerGas),
|
|
345
|
-
paymasterAndData: this.paymasterAddress as Hex,
|
|
346
|
-
signature: "0x",
|
|
347
|
-
};
|
|
277
|
+
if (support.type === "permit") throw new Error("Permit not yet supported");
|
|
278
|
+
return "NOT_NEEDED";
|
|
348
279
|
}
|
|
349
280
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
{ type: "uint256" },
|
|
362
|
-
{ type: "uint256" },
|
|
363
|
-
{ type: "uint256" },
|
|
364
|
-
{ type: "uint256" },
|
|
365
|
-
{ type: "bytes32" },
|
|
366
|
-
],
|
|
367
|
-
[
|
|
368
|
-
userOp.sender,
|
|
369
|
-
userOp.nonce,
|
|
370
|
-
keccak256(userOp.initCode),
|
|
371
|
-
keccak256(userOp.callData),
|
|
372
|
-
userOp.callGasLimit,
|
|
373
|
-
userOp.verificationGasLimit,
|
|
374
|
-
userOp.preVerificationGas,
|
|
375
|
-
userOp.maxFeePerGas,
|
|
376
|
-
userOp.maxPriorityFeePerGas,
|
|
377
|
-
keccak256(userOp.paymasterAndData),
|
|
378
|
-
]
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
const packedHash = keccak256(packed);
|
|
382
|
-
|
|
383
|
-
return keccak256(
|
|
384
|
-
encodeAbiParameters(
|
|
385
|
-
[{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
|
|
386
|
-
[packedHash, this.entryPointAddress, BigInt(this.chainConfig.chain.id)]
|
|
387
|
-
)
|
|
388
|
-
);
|
|
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);
|
|
389
292
|
}
|
|
390
293
|
|
|
391
|
-
/**
|
|
392
|
-
* Sign a UserOperation with MetaMask
|
|
393
|
-
*/
|
|
394
294
|
async signUserOperation(userOp: UserOperation): Promise<UserOperation> {
|
|
395
|
-
if (!this.owner)
|
|
396
|
-
throw new Error("Not connected");
|
|
397
|
-
}
|
|
295
|
+
if (!this.owner) throw new Error("Not connected");
|
|
398
296
|
|
|
399
|
-
const userOpHash = this.getUserOpHash(userOp);
|
|
297
|
+
const userOpHash = this.userOpBuilder.getUserOpHash(userOp);
|
|
400
298
|
|
|
401
|
-
// Sign with MetaMask using personal_sign (EIP-191)
|
|
402
299
|
const signature = (await window.ethereum!.request({
|
|
403
300
|
method: "personal_sign",
|
|
404
301
|
params: [userOpHash, this.owner],
|
|
405
302
|
})) as Hex;
|
|
406
303
|
|
|
407
|
-
return {
|
|
408
|
-
...userOp,
|
|
409
|
-
signature,
|
|
410
|
-
};
|
|
304
|
+
return { ...userOp, signature };
|
|
411
305
|
}
|
|
412
306
|
|
|
413
|
-
/**
|
|
414
|
-
* Send a signed UserOperation to the bundler
|
|
415
|
-
*/
|
|
416
307
|
async sendUserOperation(userOp: UserOperation): Promise<Hash> {
|
|
417
308
|
return this.bundlerClient.sendUserOperation(userOp);
|
|
418
309
|
}
|
|
419
310
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
*/
|
|
423
|
-
async waitForUserOperation(
|
|
424
|
-
userOpHash: Hash,
|
|
425
|
-
timeout = 60000
|
|
426
|
-
): Promise<UserOpReceipt> {
|
|
427
|
-
return this.bundlerClient.waitForUserOperation(userOpHash, timeout);
|
|
311
|
+
async waitForUserOperation(hash: Hash, timeout = 60000) {
|
|
312
|
+
return this.bundlerClient.waitForUserOperation(hash, timeout);
|
|
428
313
|
}
|
|
429
314
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
*/
|
|
434
|
-
async requestApprovalSupport(
|
|
435
|
-
token: Address,
|
|
436
|
-
spender: Address,
|
|
437
|
-
amount: bigint
|
|
438
|
-
): Promise<ApprovalSupportResult> {
|
|
439
|
-
if (!this.owner) {
|
|
440
|
-
throw new Error("Not connected");
|
|
441
|
-
}
|
|
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");
|
|
442
318
|
return this.bundlerClient.requestApprovalSupport(token, this.owner, spender, amount);
|
|
443
319
|
}
|
|
444
320
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const userOp = await this.buildDeployUserOperation();
|
|
450
|
-
const signed = await this.signUserOperation(userOp);
|
|
451
|
-
const hash = await this.sendUserOperation(signed);
|
|
452
|
-
return await this.waitForUserOperation(hash);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Approve a token for the Smart Account (EOA -> Token -> Smart Account)
|
|
457
|
-
* Checks for gas sponsorship (Relayer funding) if needed.
|
|
458
|
-
*/
|
|
459
|
-
async approveToken(
|
|
460
|
-
token: Address,
|
|
461
|
-
spender: Address,
|
|
462
|
-
amount: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129639935n // maxUint256
|
|
463
|
-
): Promise<Hash | "NOT_NEEDED"> {
|
|
464
|
-
if (!this.owner) throw new Error("Not connected");
|
|
465
|
-
|
|
466
|
-
// 1. Check if we need funding
|
|
467
|
-
const support = await this.requestApprovalSupport(token, spender, amount);
|
|
468
|
-
|
|
469
|
-
if (support.type === "approve") {
|
|
470
|
-
// 2. Encode approve data
|
|
471
|
-
const data = encodeFunctionData({
|
|
472
|
-
abi: erc20Abi,
|
|
473
|
-
functionName: "approve",
|
|
474
|
-
args: [spender, amount]
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
// 3. Send transaction via Wallet (MetaMask)
|
|
478
|
-
// If funding was needed, the Relayer has already sent ETH to this.owner
|
|
479
|
-
const txHash = await window.ethereum!.request({
|
|
480
|
-
method: "eth_sendTransaction",
|
|
481
|
-
params: [{
|
|
482
|
-
from: this.owner,
|
|
483
|
-
to: token,
|
|
484
|
-
data,
|
|
485
|
-
}]
|
|
486
|
-
}) as Hash;
|
|
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]+)/);
|
|
487
325
|
|
|
488
|
-
|
|
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 */ }
|
|
489
334
|
}
|
|
490
335
|
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
}
|
|
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");
|
|
494
338
|
|
|
495
|
-
return
|
|
339
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
496
340
|
}
|
|
497
341
|
|
|
498
342
|
// Getters
|
|
499
|
-
getOwner()
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
getSmartAccount(): Address | null {
|
|
504
|
-
return this.smartAccountAddress;
|
|
505
|
-
}
|
|
343
|
+
getOwner() { return this.owner; }
|
|
344
|
+
getSmartAccount() { return this.smartAccountAddress; }
|
|
506
345
|
}
|
|
507
346
|
|
|
508
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
|
-
};
|