@aastar/enduser 0.16.16 → 0.16.19

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.
@@ -30,6 +30,18 @@ export declare class CommunityClient extends BaseClient {
30
30
  * 3. Linking the Token to the Community in Registry
31
31
  */
32
32
  createCommunityToken(params: CreateCommunityParams, options?: TransactionOptions): Promise<Hash>;
33
+ /**
34
+ * Get Community Details (Decodes Role Metadata)
35
+ * @param communityAddress - The address of the community manager (defaults to self)
36
+ */
37
+ getCommunityInfo(communityAddress?: Address): Promise<{
38
+ name: string;
39
+ ensName: string;
40
+ website: string;
41
+ description: string;
42
+ logoURI: string;
43
+ stakeAmount: bigint;
44
+ }>;
33
45
  /**
34
46
  * Register self as a Community Manager.
35
47
  * This method handles all necessary steps:
@@ -45,6 +45,48 @@ export class CommunityClient extends BaseClient {
45
45
  throw error;
46
46
  }
47
47
  }
48
+ /**
49
+ * Get Community Details (Decodes Role Metadata)
50
+ * @param communityAddress - The address of the community manager (defaults to self)
51
+ */
52
+ async getCommunityInfo(communityAddress) {
53
+ try {
54
+ const target = communityAddress || this.getAddress();
55
+ const registryAddr = this.requireRegistry();
56
+ const registry = registryActions(registryAddr);
57
+ const publicClient = this.getStartPublicClient();
58
+ // 1. Get Role ID
59
+ const API_ROLE_COMMUNITY = await registry(publicClient).ROLE_COMMUNITY();
60
+ // 2. Fetch Metadata (Hex)
61
+ const metadataHex = await registry(publicClient).roleMetadata({
62
+ roleId: API_ROLE_COMMUNITY,
63
+ user: target
64
+ });
65
+ if (!metadataHex || metadataHex === '0x') {
66
+ throw new Error('No metadata found for this community');
67
+ }
68
+ // 3. Decode
69
+ // struct CommunityRoleData { string name; string ensName; string website; string description; string logoURI; uint256 stakeAmount; }
70
+ let dataToDecode = metadataHex;
71
+ // Check for common 'bytes' or 'tuple' wrapper offset (0x20)
72
+ if (metadataHex.startsWith('0x0000000000000000000000000000000000000000000000000000000000000020')) {
73
+ dataToDecode = `0x${metadataHex.slice(66)}`;
74
+ }
75
+ const { decodeAbiParameters, parseAbiParameters } = await import('viem');
76
+ const [name, ensName, website, description, logoURI, stakeAmount] = decodeAbiParameters(parseAbiParameters('string, string, string, string, string, uint256'), dataToDecode);
77
+ return {
78
+ name,
79
+ ensName,
80
+ website,
81
+ description,
82
+ logoURI,
83
+ stakeAmount
84
+ };
85
+ }
86
+ catch (error) {
87
+ throw error;
88
+ }
89
+ }
48
90
  /**
49
91
  * Register self as a Community Manager.
50
92
  * This method handles all necessary steps:
@@ -19,6 +19,25 @@ export declare class UserClient extends BaseClient {
19
19
  gTokenAddress?: Address;
20
20
  bundlerClient?: any;
21
21
  constructor(config: UserClientConfig);
22
+ /**
23
+ * Deploy a new Smart Account (Supports multiple factory types)
24
+ * Static helper to facilitate onboarding before instantiating the UserClient.
25
+ *
26
+ * @param client - WalletClient to sign the deployment transaction
27
+ * @param params - Deployment parameters
28
+ * @returns Object containing the deployed account address and transaction hash
29
+ */
30
+ static deployAccount(client: any, params: {
31
+ owner: Address;
32
+ salt?: bigint;
33
+ factoryAddress?: Address;
34
+ publicClient?: any;
35
+ accountType?: 'simple' | 'kernel' | 'safe' | string;
36
+ customAbi?: any;
37
+ }): Promise<{
38
+ accountAddress: Address;
39
+ hash: Hash;
40
+ }>;
22
41
  /**
23
42
  * Get the nonce of the account from EntryPoint (more reliable for 4337)
24
43
  */
@@ -20,6 +20,45 @@ export class UserClient extends BaseClient {
20
20
  this.registryAddress = config.registryAddress;
21
21
  this.gTokenAddress = config.gTokenAddress;
22
22
  }
23
+ /**
24
+ * Deploy a new Smart Account (Supports multiple factory types)
25
+ * Static helper to facilitate onboarding before instantiating the UserClient.
26
+ *
27
+ * @param client - WalletClient to sign the deployment transaction
28
+ * @param params - Deployment parameters
29
+ * @returns Object containing the deployed account address and transaction hash
30
+ */
31
+ static async deployAccount(client, params) {
32
+ const { accountFactoryActions, SimpleAccountFactoryABI } = await import('@aastar/core');
33
+ // 1. Determine Factory ABI (Ensure it's the raw ABI array)
34
+ let abi = params.customAbi || (SimpleAccountFactoryABI?.abi || SimpleAccountFactoryABI);
35
+ // In the future, we can add more built-in ABIs here based on accountType
36
+ // if (params.accountType === 'kernel') abi = KernelFactoryABI;
37
+ const factoryAddr = params.factoryAddress || '0x9406Cc6185a346906296840746125a0E44976454'; // Default v0.7 Factory
38
+ const salt = params.salt || 0n;
39
+ // Use publicClient for reading if provided, otherwise fallback to client (which might be a Full Client)
40
+ const readClient = params.publicClient || client;
41
+ // Use the generic actions with the selected ABI
42
+ const factoryRead = accountFactoryActions(factoryAddr, abi)(readClient);
43
+ const factoryWrite = accountFactoryActions(factoryAddr, abi)(client);
44
+ // 1. Predict Address
45
+ const accountAddress = await factoryRead.getAddress({
46
+ owner: params.owner,
47
+ salt
48
+ });
49
+ // 2. Deploy
50
+ try {
51
+ const hash = await factoryWrite.createAccount({
52
+ owner: params.owner,
53
+ salt,
54
+ account: client.account
55
+ });
56
+ return { accountAddress, hash };
57
+ }
58
+ catch (error) {
59
+ throw error;
60
+ }
61
+ }
23
62
  // ========================================
24
63
  // 1. 账户基本操作 (基于 L1 simpleAccountActions)
25
64
  // ========================================
@@ -330,8 +369,9 @@ export class UserClient extends BaseClient {
330
369
  args: [params.target, params.value, params.data]
331
370
  });
332
371
  // 3. Delegate to PaymasterClient for v0.7 Gasless Submission
372
+ // This ensures we follow the exact same logic as successful demo scripts
333
373
  // We dynamic import to avoid circular dependencies if any
334
- const { PaymasterClient: SDKPaymasterClient } = await import('../../paymaster/src/V4/PaymasterClient.js');
374
+ const { PaymasterClient: SDKPaymasterClient } = await import('@aastar/paymaster');
335
375
  let verificationGasLimit;
336
376
  let paymasterVerificationGasLimit;
337
377
  let paymasterPostOpGasLimit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aastar/enduser",
3
- "version": "0.16.16",
3
+ "version": "0.16.19",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,7 +20,8 @@
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "viem": "2.43.3",
23
- "@aastar/core": "0.16.16"
23
+ "@aastar/core": "0.16.19",
24
+ "@aastar/paymaster": "0.16.19"
24
25
  },
25
26
  "devDependencies": {
26
27
  "typescript": "5.7.2",
@@ -1,28 +0,0 @@
1
- import { type Address } from 'viem';
2
- /**
3
- * Bundler types we support
4
- */
5
- export declare enum BundlerType {
6
- ALCHEMY = "alchemy",
7
- PIMLICO = "pimlico",
8
- STACKUP = "stackup",
9
- CANDIDE = "candide",
10
- UNKNOWN = "unknown"
11
- }
12
- /**
13
- * Detect bundler type from URL
14
- */
15
- export declare function detectBundlerType(bundlerUrl: string): BundlerType;
16
- /**
17
- * Minimal interface to satisfy basic Pimlico/Bundler needs
18
- * without bringing in heavy permissionless types that might conflict
19
- */
20
- export interface BundlerConfig {
21
- type: BundlerType;
22
- url: string;
23
- entryPoint: Address;
24
- }
25
- /**
26
- * Create a bundler client config
27
- */
28
- export declare function createBundlerClient(bundlerUrl: string, entryPoint: Address): BundlerConfig;
@@ -1,37 +0,0 @@
1
- /**
2
- * Bundler types we support
3
- */
4
- export var BundlerType;
5
- (function (BundlerType) {
6
- BundlerType["ALCHEMY"] = "alchemy";
7
- BundlerType["PIMLICO"] = "pimlico";
8
- BundlerType["STACKUP"] = "stackup";
9
- BundlerType["CANDIDE"] = "candide";
10
- BundlerType["UNKNOWN"] = "unknown";
11
- })(BundlerType || (BundlerType = {}));
12
- /**
13
- * Detect bundler type from URL
14
- */
15
- export function detectBundlerType(bundlerUrl) {
16
- const url = bundlerUrl.toLowerCase();
17
- if (url.includes('alchemy.com'))
18
- return BundlerType.ALCHEMY;
19
- if (url.includes('pimlico.io'))
20
- return BundlerType.PIMLICO;
21
- if (url.includes('stackup'))
22
- return BundlerType.STACKUP;
23
- if (url.includes('candide.dev'))
24
- return BundlerType.CANDIDE;
25
- return BundlerType.UNKNOWN;
26
- }
27
- /**
28
- * Create a bundler client config
29
- */
30
- export function createBundlerClient(bundlerUrl, entryPoint) {
31
- const bundlerType = detectBundlerType(bundlerUrl);
32
- return {
33
- type: bundlerType,
34
- url: bundlerUrl,
35
- entryPoint
36
- };
37
- }
@@ -1,89 +0,0 @@
1
- import { type Address, type Hex } from 'viem';
2
- /**
3
- * PaymasterClient
4
- * Focus: Integration, Funding, Interaction.
5
- */
6
- export declare class PaymasterClient {
7
- /**
8
- * @private
9
- * Static utility class, should not be instantiated.
10
- */
11
- private constructor();
12
- /**
13
- * Get user's deposited balance on the Paymaster.
14
- */
15
- static getDepositedBalance(publicClient: any, address: Address, user: Address, token: Address): Promise<bigint>;
16
- /**
17
- * Deposit tokens to Paymaster for a user (enables gasless transactions).
18
- */
19
- static depositFor(wallet: any, address: Address, user: Address, token: Address, amount: bigint): Promise<any>;
20
- /**
21
- * Approve the Paymaster (or any spender) to spend gas tokens.
22
- */
23
- static approveGasToken(wallet: any, token: Address, spender: Address, amount: bigint): Promise<any>;
24
- /**
25
- * Estimate Gas for a UserOperation.
26
- */
27
- static estimateUserOperationGas(client: any, wallet: any, aaAddress: Address, entryPoint: Address, paymasterAddress: Address, token: Address, bundlerUrl: string, callData: `0x${string}`, options?: {
28
- validityWindow?: number;
29
- operator?: Address;
30
- factory?: Address;
31
- factoryData?: Hex;
32
- }): Promise<{
33
- preVerificationGas: bigint;
34
- verificationGasLimit: bigint;
35
- callGasLimit: bigint;
36
- paymasterVerificationGasLimit: bigint | undefined;
37
- paymasterPostOpGasLimit: bigint;
38
- }>;
39
- /**
40
- * High-level API to submit a gasless UserOperation.
41
- * Automatically handles nonce fetching, gas estimation (if not provided), signing, and submission.
42
- */
43
- static submitGaslessUserOperation(client: any, wallet: any, aaAddress: Address, entryPoint: Address, paymasterAddress: Address, token: Address, bundlerUrl: string, callData: `0x${string}`, options?: {
44
- validityWindow?: number;
45
- verificationGasLimit?: bigint;
46
- callGasLimit?: bigint;
47
- preVerificationGas?: bigint;
48
- maxFeePerGas?: bigint;
49
- maxPriorityFeePerGas?: bigint;
50
- autoEstimate?: boolean;
51
- operator?: Address;
52
- paymasterVerificationGasLimit?: bigint;
53
- paymasterPostOpGasLimit?: bigint;
54
- factory?: Address;
55
- factoryData?: Hex;
56
- }): Promise<`0x${string}`>;
57
- /**
58
- * Helper to extract the actual Gas Token fee from a UserOperation receipt.
59
- * Looks for the 'PostOpProcessed' event emitted by the Paymaster.
60
- */
61
- static getFeeFromReceipt(receipt: any, paymasterAddress: Address): {
62
- tokenCost: bigint;
63
- actualGasCostWei: bigint;
64
- } | null;
65
- /**
66
- * Get the fee for a specific transaction hash.
67
- * Fetches the receipt (no scanning required) and decodes the log.
68
- */
69
- static getTransactionFee(publicClient: any, txHash: `0x${string}`, paymasterAddress: Address): Promise<{
70
- tokenCost: bigint;
71
- actualGasCostWei: bigint;
72
- } | null>;
73
- /**
74
- * Helper: Encode a standardized ERC-20 Transfer.
75
- * Returns the raw function data: `transfer(to, amount)`
76
- */
77
- static encodeTokenTransfer(recipient: Address, amount: bigint): `0x${string}`;
78
- /**
79
- * Helper: Encode a SimpleAccount execution.
80
- * Wraps the inner call into: `execute(target, value, data)`
81
- * This is the payload signed by the user.
82
- */
83
- static encodeExecution(target: Address, value: bigint, data: `0x${string}`): `0x${string}`;
84
- /**
85
- * More robust version of waitForUserOperationReceipt.
86
- * Catches timeouts and returns a cleaner result.
87
- */
88
- static waitForUserOperation(bundlerClient: any, hash: `0x${string}`, timeout?: number): Promise<any>;
89
- }
@@ -1,413 +0,0 @@
1
- import { parseAbi, concat, pad, toHex, encodeFunctionData } from 'viem';
2
- import { buildPaymasterData, buildSuperPaymasterData, formatUserOpV07, getUserOpHashV07 } from './PaymasterUtils.js';
3
- import { detectBundlerType } from './BundlerCompat.js';
4
- /**
5
- * PaymasterClient
6
- * Focus: Integration, Funding, Interaction.
7
- */
8
- export class PaymasterClient {
9
- /**
10
- * @private
11
- * Static utility class, should not be instantiated.
12
- */
13
- constructor() { }
14
- /**
15
- * Get user's deposited balance on the Paymaster.
16
- */
17
- static async getDepositedBalance(publicClient, address, user, token) {
18
- return publicClient.readContract({
19
- address,
20
- abi: [{
21
- name: 'balances',
22
- type: 'function',
23
- inputs: [
24
- { name: 'user', type: 'address' },
25
- { name: 'token', type: 'address' }
26
- ],
27
- outputs: [{ name: '', type: 'uint256' }],
28
- stateMutability: 'view'
29
- }],
30
- functionName: 'balances',
31
- args: [user, token]
32
- });
33
- }
34
- /**
35
- * Deposit tokens to Paymaster for a user (enables gasless transactions).
36
- */
37
- static async depositFor(wallet, address, user, token, amount) {
38
- return wallet.writeContract({
39
- address,
40
- abi: parseAbi(['function depositFor(address user, address token, uint256 amount) external']),
41
- functionName: 'depositFor',
42
- args: [user, token, amount],
43
- chain: wallet.chain
44
- });
45
- }
46
- /**
47
- * Approve the Paymaster (or any spender) to spend gas tokens.
48
- */
49
- static async approveGasToken(wallet, token, spender, amount) {
50
- return wallet.writeContract({
51
- address: token,
52
- abi: parseAbi(['function approve(address spender, uint256 amount) external returns (bool)']),
53
- functionName: 'approve',
54
- args: [spender, amount],
55
- chain: wallet.chain
56
- });
57
- }
58
- /**
59
- * Estimate Gas for a UserOperation.
60
- */
61
- static async estimateUserOperationGas(client, wallet, aaAddress, entryPoint, paymasterAddress, token, bundlerUrl, callData, options) {
62
- // 0. Check cachedPrice (Critical for Paymaster V4)
63
- if (!options?.operator) { // Only for Paymaster V4, not SuperPaymaster
64
- try {
65
- const cache = await client.readContract({
66
- address: paymasterAddress,
67
- abi: parseAbi(['function cachedPrice() view returns (uint208 price, uint48 updatedAt)']),
68
- functionName: 'cachedPrice'
69
- });
70
- if (!cache || cache.price === 0n || cache[0] === 0n) {
71
- console.log('[PaymasterClient] ⚠️ cachedPrice is 0! Auto-initializing...');
72
- // Check if we're on testnet (chainId 11155111 = Sepolia, 11155420 = OP Sepolia)
73
- const chainId = client.chain?.id || await client.getChainId();
74
- const isTestnet = [11155111, 11155420, 31337].includes(chainId);
75
- if (isTestnet) {
76
- // Auto-call updatePrice on testnet
77
- const updateHash = await wallet.writeContract({
78
- address: paymasterAddress,
79
- abi: parseAbi(['function updatePrice() external']),
80
- functionName: 'updatePrice'
81
- });
82
- await client.waitForTransactionReceipt({ hash: updateHash });
83
- console.log('[PaymasterClient] ✅ cachedPrice initialized via updatePrice()');
84
- }
85
- else {
86
- // Mainnet: throw error, require Keeper
87
- throw new Error(`Paymaster cachedPrice is 0 on Mainnet (chainId: ${chainId}). ` +
88
- `This requires Keeper to call updatePrice(). Please ensure Keeper is running.`);
89
- }
90
- }
91
- }
92
- catch (e) {
93
- // If error is our mainnet check, re-throw
94
- if (e.message?.includes('requires Keeper'))
95
- throw e;
96
- // Otherwise log and continue (might be old Paymaster without cachedPrice)
97
- console.log('[PaymasterClient] ⚠️ Failed to check cachedPrice:', e.message?.slice(0, 50));
98
- }
99
- }
100
- // 1. Construct a dummy UserOp for estimation
101
- let paymasterAndData;
102
- if (options?.operator) {
103
- paymasterAndData = buildSuperPaymasterData(paymasterAddress, options.operator, {
104
- verificationGasLimit: 300000n,
105
- postOpGasLimit: 300000n
106
- });
107
- }
108
- else {
109
- paymasterAndData = buildPaymasterData(paymasterAddress, token, {
110
- validityWindow: options?.validityWindow,
111
- verificationGasLimit: 250000n,
112
- postOpGasLimit: 150000n
113
- });
114
- }
115
- // 1.5. Get dynamic gas prices from network
116
- let maxFeePerGas = 30000000000n; // 30 Gwei default
117
- let maxPriorityFeePerGas = 1000000000n; // 1 Gwei default
118
- try {
119
- const feeData = await client.estimateFeesPerGas();
120
- maxFeePerGas = (feeData.maxFeePerGas ?? 30000000000n) * 150n / 100n; // 1.5x buffer
121
- maxPriorityFeePerGas = (feeData.maxPriorityFeePerGas ?? 1000000000n) * 150n / 100n;
122
- if (maxPriorityFeePerGas < 500000000n)
123
- maxPriorityFeePerGas = 500000000n; // Min 0.5 Gwei
124
- }
125
- catch (e) {
126
- // Use defaults if estimation fails
127
- }
128
- const partialUserOp = {
129
- sender: aaAddress,
130
- nonce: 0n,
131
- initCode: (options?.factory && options?.factoryData)
132
- ? concat([options.factory, options.factoryData])
133
- : '0x',
134
- callData,
135
- accountGasLimits: concat([pad(toHex(250000n), { size: 16 }), pad(toHex(500000n), { size: 16 })]),
136
- preVerificationGas: 100000n,
137
- gasFees: concat([pad(toHex(maxPriorityFeePerGas), { size: 16 }), pad(toHex(maxFeePerGas), { size: 16 })]),
138
- paymasterAndData,
139
- signature: '0x'
140
- };
141
- // Get actual nonce
142
- try {
143
- const nonce = await client.readContract({
144
- address: aaAddress,
145
- abi: parseAbi(['function getNonce() view returns (uint256)']),
146
- functionName: 'getNonce'
147
- });
148
- partialUserOp.nonce = BigInt(nonce);
149
- }
150
- catch (e) { }
151
- const userOpHash = getUserOpHashV07(partialUserOp, entryPoint, BigInt(client.chain.id));
152
- partialUserOp.signature = (await wallet.account.signMessage({ message: { raw: userOpHash } }));
153
- const payload = {
154
- jsonrpc: '2.0',
155
- id: 1,
156
- method: 'eth_estimateUserOperationGas',
157
- params: [formatUserOpV07(partialUserOp), entryPoint]
158
- };
159
- const response = await fetch(bundlerUrl, {
160
- method: 'POST',
161
- headers: { 'Content-Type': 'application/json' },
162
- body: JSON.stringify(payload, (_, v) => typeof v === 'bigint' ? '0x' + v.toString(16) : v)
163
- });
164
- const result = await response.json();
165
- // Debug logging for Candide
166
- if (bundlerUrl.includes('candide')) {
167
- console.log('[PaymasterClient] Candide Request:', JSON.stringify(payload.params[0], null, 2));
168
- console.log('[PaymasterClient] Candide Response:', JSON.stringify(result, null, 2));
169
- }
170
- const data = result.result;
171
- // Debug logging for estimation
172
- console.log('[PaymasterClient] Gas Estimation Result:', JSON.stringify(data, null, 2));
173
- // Anvil Fallback for Estimation
174
- if (result.error && (result.error.code === -32601 || result.error.message?.includes('Method not found'))) {
175
- console.log('[PaymasterClient] EstimateUserOp failed (Method not found). Using Anvil defaults.');
176
- return {
177
- preVerificationGas: 100000n,
178
- verificationGasLimit: 1000000n,
179
- callGasLimit: 2000000n,
180
- paymasterVerificationGasLimit: 100000n,
181
- paymasterPostOpGasLimit: 100000n
182
- };
183
- }
184
- if (result.error)
185
- throw new Error(`Estimation Error: ${JSON.stringify(result.error)}`);
186
- // Dynamic tuning: use estimated values directly to maintain efficiency
187
- // Bundler efficiency check: actual_used / limit >= 0.4
188
- return {
189
- preVerificationGas: BigInt(data.preVerificationGas),
190
- verificationGasLimit: BigInt(data.verificationGasLimit), // Use estimate as-is for efficiency
191
- callGasLimit: (BigInt(data.callGasLimit) * 110n) / 100n, // Small 1.1x buffer
192
- paymasterVerificationGasLimit: data.paymasterVerificationGasLimit ? BigInt(data.paymasterVerificationGasLimit) : undefined,
193
- paymasterPostOpGasLimit: data.paymasterPostOpGasLimit ? BigInt(data.paymasterPostOpGasLimit) : 100000n
194
- };
195
- }
196
- /**
197
- * High-level API to submit a gasless UserOperation.
198
- * Automatically handles nonce fetching, gas estimation (if not provided), signing, and submission.
199
- */
200
- static async submitGaslessUserOperation(client, wallet, aaAddress, entryPoint, paymasterAddress, token, bundlerUrl, callData, options) {
201
- // 0. Auto-Estimate if requested or if limits missing
202
- let gasLimits = {
203
- preVerificationGas: options?.preVerificationGas,
204
- verificationGasLimit: options?.verificationGasLimit,
205
- callGasLimit: options?.callGasLimit,
206
- paymasterVerificationGasLimit: options?.paymasterVerificationGasLimit,
207
- paymasterPostOpGasLimit: options?.paymasterPostOpGasLimit ?? 150000n
208
- };
209
- if (options?.autoEstimate !== false && (!gasLimits.verificationGasLimit || !gasLimits.callGasLimit)) {
210
- const est = await this.estimateUserOperationGas(client, wallet, aaAddress, entryPoint, paymasterAddress, token, bundlerUrl, callData, {
211
- validityWindow: options?.validityWindow,
212
- operator: options?.operator,
213
- factory: options?.factory,
214
- factoryData: options?.factoryData
215
- });
216
- gasLimits.preVerificationGas = options?.preVerificationGas ?? est.preVerificationGas;
217
- gasLimits.verificationGasLimit = options?.verificationGasLimit ?? est.verificationGasLimit;
218
- gasLimits.callGasLimit = options?.callGasLimit ?? est.callGasLimit;
219
- gasLimits.paymasterVerificationGasLimit = options?.paymasterVerificationGasLimit ?? est.paymasterVerificationGasLimit;
220
- gasLimits.paymasterPostOpGasLimit = options?.paymasterPostOpGasLimit ?? est.paymasterPostOpGasLimit;
221
- }
222
- // 1. Get Nonce
223
- const nonce = await client.readContract({
224
- address: aaAddress,
225
- abi: parseAbi(['function getNonce() view returns (uint256)']),
226
- functionName: 'getNonce'
227
- });
228
- // 1.5 Get Gas Prices from Network if not provided
229
- let maxFeePerGas = options?.maxFeePerGas;
230
- let maxPriorityFeePerGas = options?.maxPriorityFeePerGas;
231
- if (!maxFeePerGas || !maxPriorityFeePerGas) {
232
- try {
233
- const feeData = await client.estimateFeesPerGas();
234
- // Apply 1.5x buffer for network volatility
235
- maxFeePerGas = maxFeePerGas ?? ((feeData.maxFeePerGas ?? 30000000000n) * 150n) / 100n;
236
- maxPriorityFeePerGas = maxPriorityFeePerGas ?? ((feeData.maxPriorityFeePerGas ?? 1000000000n) * 150n) / 100n;
237
- if (maxPriorityFeePerGas < 500000000n) {
238
- console.log(`[PaymasterClient] Priority Fee ${maxPriorityFeePerGas} too low, clamping to 0.5 Gwei`);
239
- maxPriorityFeePerGas = 500000000n; // Min 0.5 Gwei
240
- }
241
- // Ensure MaxFee >= Priority
242
- if (maxFeePerGas < maxPriorityFeePerGas) {
243
- maxFeePerGas = maxPriorityFeePerGas + 1000000n; // Add small buffer
244
- console.log(`[PaymasterClient] MaxFee bumped to accommodate Priority Fee`);
245
- }
246
- console.log(`[PaymasterClient] Fees Set: MaxFee=${maxFeePerGas}, Priority=${maxPriorityFeePerGas}`);
247
- }
248
- catch (e) {
249
- console.log('[PaymasterClient] Fee Estimation Failed:', e);
250
- // Fallback to safer defaults if estimation fails
251
- maxFeePerGas = maxFeePerGas ?? 50000000000n; // 50 Gwei
252
- maxPriorityFeePerGas = maxPriorityFeePerGas ?? 2000000000n; // 2 Gwei
253
- }
254
- }
255
- // 2. Build paymasterAndData
256
- let paymasterAndData;
257
- if (options?.operator) {
258
- paymasterAndData = buildSuperPaymasterData(paymasterAddress, options.operator, {
259
- verificationGasLimit: gasLimits.paymasterVerificationGasLimit ?? gasLimits.verificationGasLimit ?? 150000n,
260
- postOpGasLimit: gasLimits.paymasterPostOpGasLimit ?? 100000n
261
- });
262
- }
263
- else {
264
- // MATH: Target Efficiency = PVG / (PVG + VGL + PMVGL) >= 0.4
265
- // Since PVG is ~100k, (VGL + PMVGL) must be <= 150k.
266
- // We set each to 75k to safely pass the 0.4 efficiency guard.
267
- const pmVerGas = 75000n;
268
- paymasterAndData = buildPaymasterData(paymasterAddress, token, {
269
- validityWindow: options?.validityWindow,
270
- verificationGasLimit: pmVerGas,
271
- postOpGasLimit: gasLimits.paymasterPostOpGasLimit ?? 100000n
272
- });
273
- }
274
- // 3. Construct UserOp
275
- const userOp = {
276
- sender: aaAddress,
277
- nonce: BigInt(nonce),
278
- initCode: (options?.factory && options?.factoryData)
279
- ? concat([options.factory, options.factoryData])
280
- : '0x',
281
- callData,
282
- accountGasLimits: concat([
283
- pad(toHex(75000n), { size: 16 }), // Verification (Tuned for 0.4 efficiency)
284
- pad(toHex(gasLimits.callGasLimit ?? 500000n), { size: 16 }) // Call
285
- ]),
286
- preVerificationGas: gasLimits.preVerificationGas ?? 50000n,
287
- gasFees: concat([
288
- pad(toHex(maxPriorityFeePerGas), { size: 16 }),
289
- pad(toHex(maxFeePerGas), { size: 16 })
290
- ]),
291
- paymasterAndData,
292
- signature: '0x'
293
- };
294
- // Debug logs (Commented out for production)
295
- /*
296
- console.log("DEBUG: UserOp Gas Limits:", {
297
- accountGasLimits: userOp.accountGasLimits,
298
- preVerificationGas: userOp.preVerificationGas,
299
- gasFees: userOp.gasFees,
300
- paymasterAndData: userOp.paymasterAndData
301
- });
302
- */
303
- // 4. Final Hashing and Signing
304
- const userOpHash = getUserOpHashV07(userOp, entryPoint, BigInt(client.chain.id));
305
- const signature = (await wallet.account.signMessage({ message: { raw: userOpHash } }));
306
- userOp.signature = signature;
307
- // 6. Submit to Bundler (Unified JSON-RPC)
308
- const bundlerType = detectBundlerType(bundlerUrl);
309
- console.log(`[PaymasterClient] Using ${bundlerType} Bundler`);
310
- // Use standard JSON-RPC for all bundlers (Pimlico/Alchemy/Stackup/etc)
311
- const response = await fetch(bundlerUrl, {
312
- method: 'POST',
313
- headers: { 'Content-Type': 'application/json' },
314
- body: JSON.stringify({
315
- jsonrpc: '2.0',
316
- id: 1,
317
- method: 'eth_sendUserOperation',
318
- params: [formatUserOpV07(userOp), entryPoint]
319
- }, (_, v) => typeof v === 'bigint' ? '0x' + v.toString(16) : v)
320
- });
321
- const result = await response.json();
322
- if (result.error && (result.error.code === -32601 || result.error.message?.includes('Method not found'))) {
323
- console.log('[PaymasterClient] SendUserOp failed (Method not found). Falling back to direct handleOps...');
324
- const caller = wallet.account?.address ? wallet.account.address : wallet.account;
325
- return await wallet.writeContract({
326
- address: entryPoint,
327
- abi: parseAbi(['function handleOps((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[], address) external']),
328
- functionName: 'handleOps',
329
- args: [[userOp], caller],
330
- chain: wallet.chain,
331
- account: wallet.account
332
- });
333
- }
334
- if (result.error)
335
- throw new Error(`Bundler Error: ${JSON.stringify(result.error)}`);
336
- console.log('[PaymasterClient] ✅ Submitted via', bundlerType, 'hash:', result.result);
337
- return result.result;
338
- }
339
- /**
340
- * Helper to extract the actual Gas Token fee from a UserOperation receipt.
341
- * Looks for the 'PostOpProcessed' event emitted by the Paymaster.
342
- */
343
- static getFeeFromReceipt(receipt, paymasterAddress) {
344
- // Event Signature: PostOpProcessed(address indexed user, address indexed token, uint256 actualGasCostWei, uint256 tokenCost, uint256 protocolRevenue)
345
- // Topic0: 0x62544d7f48b11c32334310ebd306b47224fca220163218d4a7264322c52ae073
346
- const TOPIC_POST_OP = '0x62544d7f48b11c32334310ebd306b47224fca220163218d4a7264322c52ae073';
347
- for (const log of receipt.logs) {
348
- if (log.address.toLowerCase() === paymasterAddress.toLowerCase() && log.topics[0] === TOPIC_POST_OP) {
349
- // Decode Data: actualGasCostWei, tokenCost, protocolRevenue (3x uint256)
350
- // We manually decode or use viem's decodeEventLog if available.
351
- // Here we use a lightweight manual decode for the data part (non-indexed).
352
- // Data is 3 * 32 bytes.
353
- const data = log.data.replace('0x', '');
354
- if (data.length >= 192) { // 3 * 64 hex chars = 192
355
- const actualGasCostWei = BigInt('0x' + data.slice(0, 64));
356
- const tokenCost = BigInt('0x' + data.slice(64, 128));
357
- // const protocolRevenue = BigInt('0x' + data.slice(128, 192));
358
- return { tokenCost, actualGasCostWei };
359
- }
360
- }
361
- }
362
- return null;
363
- }
364
- /**
365
- * Get the fee for a specific transaction hash.
366
- * Fetches the receipt (no scanning required) and decodes the log.
367
- */
368
- static async getTransactionFee(publicClient, txHash, paymasterAddress) {
369
- const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
370
- return this.getFeeFromReceipt(receipt, paymasterAddress);
371
- }
372
- // ===========================================
373
- // 🛠️ Semantic CallData Builders (For DX)
374
- // ===========================================
375
- /**
376
- * Helper: Encode a standardized ERC-20 Transfer.
377
- * Returns the raw function data: `transfer(to, amount)`
378
- */
379
- static encodeTokenTransfer(recipient, amount) {
380
- return encodeFunctionData({
381
- abi: parseAbi(['function transfer(address to, uint256 amount) external returns (bool)']),
382
- functionName: 'transfer',
383
- args: [recipient, amount]
384
- });
385
- }
386
- /**
387
- * Helper: Encode a SimpleAccount execution.
388
- * Wraps the inner call into: `execute(target, value, data)`
389
- * This is the payload signed by the user.
390
- */
391
- static encodeExecution(target, value, data) {
392
- return encodeFunctionData({
393
- abi: parseAbi(['function execute(address dest, uint256 value, bytes func) external']),
394
- functionName: 'execute',
395
- args: [target, value, data]
396
- });
397
- }
398
- /**
399
- * More robust version of waitForUserOperationReceipt.
400
- * Catches timeouts and returns a cleaner result.
401
- */
402
- static async waitForUserOperation(bundlerClient, hash, timeout = 60000) {
403
- try {
404
- return await bundlerClient.waitForUserOperationReceipt({ hash, timeout });
405
- }
406
- catch (error) {
407
- if (error.name === 'TimeoutError' || error.message?.includes('timed out')) {
408
- return { timeout: true, hash };
409
- }
410
- throw error;
411
- }
412
- }
413
- }