@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.
- package/dist/enduser/src/CommunityClient.d.ts +12 -0
- package/dist/enduser/src/CommunityClient.js +42 -0
- package/dist/enduser/src/UserClient.d.ts +19 -0
- package/dist/enduser/src/UserClient.js +41 -1
- package/package.json +3 -2
- package/dist/paymaster/src/V4/BundlerCompat.d.ts +0 -28
- package/dist/paymaster/src/V4/BundlerCompat.js +0 -37
- package/dist/paymaster/src/V4/PaymasterClient.d.ts +0 -89
- package/dist/paymaster/src/V4/PaymasterClient.js +0 -413
|
@@ -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('
|
|
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.
|
|
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.
|
|
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
|
-
}
|