@gasfree-kit/evm-4337 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ import * as _tetherto_wdk_wallet_evm_erc_4337 from '@tetherto/wdk-wallet-evm-erc-4337';
2
+ import * as _gasfree_kit_core from '@gasfree-kit/core';
3
+ import { EvmChain } from '@gasfree-kit/core';
4
+
5
+ type EvmErc4337NetworkConfig = {
6
+ chainId: number;
7
+ provider: string;
8
+ bundlerUrl: string;
9
+ paymasterUrl: string;
10
+ paymasterAddress: string;
11
+ entryPointAddress: string;
12
+ safeModulesVersion: string;
13
+ transferMaxFee: number;
14
+ isSponsored: boolean;
15
+ sponsorshipPolicyId: string;
16
+ paymasterToken: {
17
+ address: string;
18
+ };
19
+ };
20
+ type EVM4337ClientConfig = {
21
+ chain: EvmChain;
22
+ rpcUrl: string;
23
+ bundlerUrl: string;
24
+ paymasterUrl: string;
25
+ entryPointAddress?: string;
26
+ safeModulesVersion?: string;
27
+ /** Must be explicitly set. When true, requires sponsorshipPolicyId. */
28
+ isSponsored: boolean;
29
+ sponsorshipPolicyId?: string;
30
+ paymasterAddress?: string;
31
+ paymasterTokenAddress?: string;
32
+ };
33
+ /**
34
+ * Build the full ERC-4337 network config from user-provided client config.
35
+ * Matches the structure expected by @tetherto/wdk-wallet-evm-erc-4337.
36
+ */
37
+ declare function getErc4337ConfigForChain(config: EVM4337ClientConfig): EvmErc4337NetworkConfig;
38
+ /** Conservative gas fee fallback estimates per chain (USDT 6 decimals). */
39
+ declare const GAS_FEE_FALLBACKS: Record<string, bigint>;
40
+ /** Fallback fee estimates as human-readable strings. */
41
+ declare const GAS_FEE_ESTIMATES: Record<string, string>;
42
+
43
+ /**
44
+ * Set up an ERC-4337 wallet via WDK.
45
+ *
46
+ * Dynamically imports @tetherto/wdk-wallet-evm-erc-4337 and builds the
47
+ * correct discriminated-union config:
48
+ * - Sponsored mode → EvmErc4337WalletSponsorshipPolicyConfig
49
+ * - Non-sponsored → EvmErc4337WalletPaymasterTokenConfig
50
+ */
51
+ declare function setupErc4337Wallet(seedPhrase: string, config: EVM4337ClientConfig, accountIndex?: number): Promise<{
52
+ wallet: _tetherto_wdk_wallet_evm_erc_4337.default;
53
+ account: _tetherto_wdk_wallet_evm_erc_4337.EvmAccount;
54
+ address: string;
55
+ }>;
56
+
57
+ declare class TetherEVMERC4337Transfer {
58
+ /** Default timeout for UserOp confirmation: 120 seconds. */
59
+ private static readonly USER_OP_TIMEOUT_MS;
60
+ /**
61
+ * Wait for a UserOperation to be included on-chain via the Candide bundler.
62
+ * Times out after USER_OP_TIMEOUT_MS to prevent indefinite hangs.
63
+ */
64
+ private static waitForUserOpConfirmation;
65
+ /**
66
+ * Get transaction fee estimate for a USDT transfer.
67
+ *
68
+ * Uses a minimal quote amount (gas cost is the same regardless of transfer
69
+ * amount). Applies a gas buffer (35% Ethereum, 30% others) for volatility.
70
+ */
71
+ static getTransactionEstimateFee(seedPhrase: string, config: EVM4337ClientConfig, recipientAddress: string, accountIndex?: number): Promise<{
72
+ message: string;
73
+ success: boolean;
74
+ data: {
75
+ fee: string;
76
+ };
77
+ }>;
78
+ /**
79
+ * Check token balance on an EVM chain.
80
+ */
81
+ static checkTokenBalance(seedPhrase: string, config: EVM4337ClientConfig, tokenAddress: string): Promise<{
82
+ message: string;
83
+ success: boolean;
84
+ data: {
85
+ tokenBalance: string;
86
+ decimals: number;
87
+ usdBalance: string;
88
+ };
89
+ }>;
90
+ /**
91
+ * Send a single USDT transfer via ERC-4337.
92
+ *
93
+ * Handles:
94
+ * - Balance validation
95
+ * - Gas fee auto-adjustment for non-sponsored mode
96
+ * - On-chain confirmation via Candide bundler
97
+ * - User-friendly error mapping
98
+ */
99
+ static sendToken(seedPhrase: string, config: EVM4337ClientConfig, tokenAddress: string, amount: string, recipientAddress: string, accountIndex?: number): Promise<{
100
+ success: boolean;
101
+ transactionHash: string;
102
+ chain: _gasfree_kit_core.EvmChain;
103
+ amount: string;
104
+ }>;
105
+ /**
106
+ * Send a batch USDT transfer to multiple recipients in a single UserOperation.
107
+ */
108
+ static sendBatchToken(seedPhrase: string, config: EVM4337ClientConfig, tokenAddress: string, recipients: Array<{
109
+ address: string;
110
+ amount: string;
111
+ }>, accountIndex?: number): Promise<{
112
+ success: boolean;
113
+ transactionHash: string;
114
+ chain: _gasfree_kit_core.EvmChain;
115
+ recipients: {
116
+ address: string;
117
+ amount: string;
118
+ }[];
119
+ }>;
120
+ /**
121
+ * Map raw errors into user-friendly messages.
122
+ */
123
+ static handleTransferError(error: unknown, chain: string): Error;
124
+ }
125
+
126
+ /**
127
+ * Convert USDT amount to base units (BigInt).
128
+ * USDT on EVM chains uses 6 decimals.
129
+ */
130
+ declare function toUsdtBaseUnitsEvm(amount: number | string): bigint;
131
+ /**
132
+ * Convert a raw token balance (BigInt) to a human-readable string.
133
+ */
134
+ declare function formatTokenBalance(rawBalance: bigint, decimals?: number, precision?: number): string;
135
+ /**
136
+ * Encode an ERC-20 transfer(address,uint256) call.
137
+ * Uses proper ABI encoding with validation to prevent malformed calldata.
138
+ */
139
+ declare function encodeErc20Transfer(to: string, amount: bigint): string;
140
+ /**
141
+ * Convert gas fee in wei to USDT value using native token price.
142
+ * Uses BigInt arithmetic to avoid precision loss for large wei values.
143
+ */
144
+ declare function feeToUsdt(feeInWei: bigint, nativeTokenPriceUsdt: number): string;
145
+
146
+ export { type EVM4337ClientConfig, type EvmErc4337NetworkConfig, GAS_FEE_ESTIMATES, GAS_FEE_FALLBACKS, TetherEVMERC4337Transfer, encodeErc20Transfer, feeToUsdt, formatTokenBalance, getErc4337ConfigForChain, setupErc4337Wallet, toUsdtBaseUnitsEvm };
@@ -0,0 +1,146 @@
1
+ import * as _tetherto_wdk_wallet_evm_erc_4337 from '@tetherto/wdk-wallet-evm-erc-4337';
2
+ import * as _gasfree_kit_core from '@gasfree-kit/core';
3
+ import { EvmChain } from '@gasfree-kit/core';
4
+
5
+ type EvmErc4337NetworkConfig = {
6
+ chainId: number;
7
+ provider: string;
8
+ bundlerUrl: string;
9
+ paymasterUrl: string;
10
+ paymasterAddress: string;
11
+ entryPointAddress: string;
12
+ safeModulesVersion: string;
13
+ transferMaxFee: number;
14
+ isSponsored: boolean;
15
+ sponsorshipPolicyId: string;
16
+ paymasterToken: {
17
+ address: string;
18
+ };
19
+ };
20
+ type EVM4337ClientConfig = {
21
+ chain: EvmChain;
22
+ rpcUrl: string;
23
+ bundlerUrl: string;
24
+ paymasterUrl: string;
25
+ entryPointAddress?: string;
26
+ safeModulesVersion?: string;
27
+ /** Must be explicitly set. When true, requires sponsorshipPolicyId. */
28
+ isSponsored: boolean;
29
+ sponsorshipPolicyId?: string;
30
+ paymasterAddress?: string;
31
+ paymasterTokenAddress?: string;
32
+ };
33
+ /**
34
+ * Build the full ERC-4337 network config from user-provided client config.
35
+ * Matches the structure expected by @tetherto/wdk-wallet-evm-erc-4337.
36
+ */
37
+ declare function getErc4337ConfigForChain(config: EVM4337ClientConfig): EvmErc4337NetworkConfig;
38
+ /** Conservative gas fee fallback estimates per chain (USDT 6 decimals). */
39
+ declare const GAS_FEE_FALLBACKS: Record<string, bigint>;
40
+ /** Fallback fee estimates as human-readable strings. */
41
+ declare const GAS_FEE_ESTIMATES: Record<string, string>;
42
+
43
+ /**
44
+ * Set up an ERC-4337 wallet via WDK.
45
+ *
46
+ * Dynamically imports @tetherto/wdk-wallet-evm-erc-4337 and builds the
47
+ * correct discriminated-union config:
48
+ * - Sponsored mode → EvmErc4337WalletSponsorshipPolicyConfig
49
+ * - Non-sponsored → EvmErc4337WalletPaymasterTokenConfig
50
+ */
51
+ declare function setupErc4337Wallet(seedPhrase: string, config: EVM4337ClientConfig, accountIndex?: number): Promise<{
52
+ wallet: _tetherto_wdk_wallet_evm_erc_4337.default;
53
+ account: _tetherto_wdk_wallet_evm_erc_4337.EvmAccount;
54
+ address: string;
55
+ }>;
56
+
57
+ declare class TetherEVMERC4337Transfer {
58
+ /** Default timeout for UserOp confirmation: 120 seconds. */
59
+ private static readonly USER_OP_TIMEOUT_MS;
60
+ /**
61
+ * Wait for a UserOperation to be included on-chain via the Candide bundler.
62
+ * Times out after USER_OP_TIMEOUT_MS to prevent indefinite hangs.
63
+ */
64
+ private static waitForUserOpConfirmation;
65
+ /**
66
+ * Get transaction fee estimate for a USDT transfer.
67
+ *
68
+ * Uses a minimal quote amount (gas cost is the same regardless of transfer
69
+ * amount). Applies a gas buffer (35% Ethereum, 30% others) for volatility.
70
+ */
71
+ static getTransactionEstimateFee(seedPhrase: string, config: EVM4337ClientConfig, recipientAddress: string, accountIndex?: number): Promise<{
72
+ message: string;
73
+ success: boolean;
74
+ data: {
75
+ fee: string;
76
+ };
77
+ }>;
78
+ /**
79
+ * Check token balance on an EVM chain.
80
+ */
81
+ static checkTokenBalance(seedPhrase: string, config: EVM4337ClientConfig, tokenAddress: string): Promise<{
82
+ message: string;
83
+ success: boolean;
84
+ data: {
85
+ tokenBalance: string;
86
+ decimals: number;
87
+ usdBalance: string;
88
+ };
89
+ }>;
90
+ /**
91
+ * Send a single USDT transfer via ERC-4337.
92
+ *
93
+ * Handles:
94
+ * - Balance validation
95
+ * - Gas fee auto-adjustment for non-sponsored mode
96
+ * - On-chain confirmation via Candide bundler
97
+ * - User-friendly error mapping
98
+ */
99
+ static sendToken(seedPhrase: string, config: EVM4337ClientConfig, tokenAddress: string, amount: string, recipientAddress: string, accountIndex?: number): Promise<{
100
+ success: boolean;
101
+ transactionHash: string;
102
+ chain: _gasfree_kit_core.EvmChain;
103
+ amount: string;
104
+ }>;
105
+ /**
106
+ * Send a batch USDT transfer to multiple recipients in a single UserOperation.
107
+ */
108
+ static sendBatchToken(seedPhrase: string, config: EVM4337ClientConfig, tokenAddress: string, recipients: Array<{
109
+ address: string;
110
+ amount: string;
111
+ }>, accountIndex?: number): Promise<{
112
+ success: boolean;
113
+ transactionHash: string;
114
+ chain: _gasfree_kit_core.EvmChain;
115
+ recipients: {
116
+ address: string;
117
+ amount: string;
118
+ }[];
119
+ }>;
120
+ /**
121
+ * Map raw errors into user-friendly messages.
122
+ */
123
+ static handleTransferError(error: unknown, chain: string): Error;
124
+ }
125
+
126
+ /**
127
+ * Convert USDT amount to base units (BigInt).
128
+ * USDT on EVM chains uses 6 decimals.
129
+ */
130
+ declare function toUsdtBaseUnitsEvm(amount: number | string): bigint;
131
+ /**
132
+ * Convert a raw token balance (BigInt) to a human-readable string.
133
+ */
134
+ declare function formatTokenBalance(rawBalance: bigint, decimals?: number, precision?: number): string;
135
+ /**
136
+ * Encode an ERC-20 transfer(address,uint256) call.
137
+ * Uses proper ABI encoding with validation to prevent malformed calldata.
138
+ */
139
+ declare function encodeErc20Transfer(to: string, amount: bigint): string;
140
+ /**
141
+ * Convert gas fee in wei to USDT value using native token price.
142
+ * Uses BigInt arithmetic to avoid precision loss for large wei values.
143
+ */
144
+ declare function feeToUsdt(feeInWei: bigint, nativeTokenPriceUsdt: number): string;
145
+
146
+ export { type EVM4337ClientConfig, type EvmErc4337NetworkConfig, GAS_FEE_ESTIMATES, GAS_FEE_FALLBACKS, TetherEVMERC4337Transfer, encodeErc20Transfer, feeToUsdt, formatTokenBalance, getErc4337ConfigForChain, setupErc4337Wallet, toUsdtBaseUnitsEvm };
package/dist/index.js ADDED
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ GAS_FEE_ESTIMATES: () => GAS_FEE_ESTIMATES,
34
+ GAS_FEE_FALLBACKS: () => GAS_FEE_FALLBACKS,
35
+ TetherEVMERC4337Transfer: () => TetherEVMERC4337Transfer,
36
+ encodeErc20Transfer: () => encodeErc20Transfer,
37
+ feeToUsdt: () => feeToUsdt,
38
+ formatTokenBalance: () => formatTokenBalance,
39
+ getErc4337ConfigForChain: () => getErc4337ConfigForChain,
40
+ setupErc4337Wallet: () => setupErc4337Wallet,
41
+ toUsdtBaseUnitsEvm: () => toUsdtBaseUnitsEvm
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/evmNetworkConfiguration.ts
46
+ var import_core = require("@gasfree-kit/core");
47
+ var DEFAULT_ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
48
+ var DEFAULT_SAFE_MODULES_VERSION = "0.3.0";
49
+ var DEFAULT_PAYMASTER_ADDRESS = "0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba";
50
+ var DEFAULT_TRANSFER_MAX_FEE = 1e15;
51
+ function getErc4337ConfigForChain(config) {
52
+ const chainConfig = import_core.EVM_CHAINS[config.chain];
53
+ return {
54
+ chainId: chainConfig.chainId,
55
+ provider: config.rpcUrl,
56
+ bundlerUrl: config.bundlerUrl,
57
+ paymasterUrl: config.paymasterUrl,
58
+ paymasterAddress: config.paymasterAddress ?? DEFAULT_PAYMASTER_ADDRESS,
59
+ entryPointAddress: config.entryPointAddress ?? DEFAULT_ENTRY_POINT,
60
+ safeModulesVersion: config.safeModulesVersion ?? DEFAULT_SAFE_MODULES_VERSION,
61
+ transferMaxFee: DEFAULT_TRANSFER_MAX_FEE,
62
+ isSponsored: config.isSponsored,
63
+ sponsorshipPolicyId: config.sponsorshipPolicyId ?? "",
64
+ paymasterToken: {
65
+ address: config.paymasterTokenAddress ?? chainConfig.usdtAddress
66
+ }
67
+ };
68
+ }
69
+ var GAS_FEE_FALLBACKS = {
70
+ ethereum: 1400000n,
71
+ arbitrum: 100000n,
72
+ polygon: 100000n,
73
+ celo: 50000n,
74
+ optimism: 100000n,
75
+ base: 150000n
76
+ };
77
+ var GAS_FEE_ESTIMATES = {
78
+ ethereum: "1.40",
79
+ arbitrum: "0.10",
80
+ polygon: "0.10",
81
+ celo: "0.05",
82
+ optimism: "0.10",
83
+ base: "0.15"
84
+ };
85
+
86
+ // src/evmERC4337.ts
87
+ async function setupErc4337Wallet(seedPhrase, config, accountIndex = 0) {
88
+ const networkConfig = getErc4337ConfigForChain(config);
89
+ const WalletManagerEvmErc4337Module = await import("@tetherto/wdk-wallet-evm-erc-4337");
90
+ const WalletManagerEvmErc4337 = WalletManagerEvmErc4337Module.default;
91
+ const wdkConfig = networkConfig.isSponsored ? {
92
+ chainId: networkConfig.chainId,
93
+ provider: networkConfig.provider,
94
+ bundlerUrl: networkConfig.bundlerUrl,
95
+ entryPointAddress: networkConfig.entryPointAddress,
96
+ safeModulesVersion: networkConfig.safeModulesVersion,
97
+ isSponsored: true,
98
+ paymasterUrl: networkConfig.paymasterUrl,
99
+ sponsorshipPolicyId: networkConfig.sponsorshipPolicyId
100
+ } : {
101
+ chainId: networkConfig.chainId,
102
+ provider: networkConfig.provider,
103
+ bundlerUrl: networkConfig.bundlerUrl,
104
+ entryPointAddress: networkConfig.entryPointAddress,
105
+ safeModulesVersion: networkConfig.safeModulesVersion,
106
+ paymasterUrl: networkConfig.paymasterUrl,
107
+ paymasterAddress: networkConfig.paymasterAddress,
108
+ paymasterToken: networkConfig.paymasterToken
109
+ };
110
+ const wallet = new WalletManagerEvmErc4337(
111
+ seedPhrase,
112
+ wdkConfig
113
+ );
114
+ const account = await wallet.getAccount(accountIndex);
115
+ const address = await account.getAddress();
116
+ return { wallet, account, address };
117
+ }
118
+
119
+ // src/evmERC4337TetherTransfer.ts
120
+ var import_abstractionkit = require("abstractionkit");
121
+ var import_core2 = require("@gasfree-kit/core");
122
+
123
+ // src/evmUtility.ts
124
+ function toUsdtBaseUnitsEvm(amount) {
125
+ const str = amount.toString().trim();
126
+ if (!/^\d+(\.\d+)?$/.test(str)) {
127
+ throw new Error(`Invalid USDT amount: "${str}". Must be a non-negative decimal number.`);
128
+ }
129
+ const decimals = 6;
130
+ const [integer, fraction = ""] = str.split(".");
131
+ if (fraction.length > decimals) {
132
+ throw new Error(
133
+ `USDT amount has too many decimal places (${fraction.length}). Maximum is ${decimals}.`
134
+ );
135
+ }
136
+ const normalizedFraction = (fraction + "0".repeat(decimals)).slice(0, decimals);
137
+ return BigInt(integer + normalizedFraction);
138
+ }
139
+ function formatTokenBalance(rawBalance, decimals = 18, precision = 6) {
140
+ const divisor = 10n ** BigInt(decimals);
141
+ const whole = rawBalance / divisor;
142
+ const fraction = rawBalance % divisor;
143
+ const fractionStr = fraction.toString().padStart(decimals, "0").slice(0, precision);
144
+ const result = `${whole}.${fractionStr}`.replace(/\.?0+$/, "");
145
+ return result || "0";
146
+ }
147
+ function encodeErc20Transfer(to, amount) {
148
+ const TRANSFER_SELECTOR = "0xa9059cbb";
149
+ const addr = to.startsWith("0x") ? to.slice(2) : to;
150
+ if (addr.length !== 40) {
151
+ throw new Error(`Invalid address length for ABI encoding: ${to}`);
152
+ }
153
+ const amountHex = amount.toString(16);
154
+ if (amountHex.length > 64) {
155
+ throw new Error(`Amount too large for uint256 encoding: ${amount}`);
156
+ }
157
+ const paddedAddress = addr.toLowerCase().padStart(64, "0");
158
+ const paddedAmount = amountHex.padStart(64, "0");
159
+ return `${TRANSFER_SELECTOR}${paddedAddress}${paddedAmount}`;
160
+ }
161
+ function feeToUsdt(feeInWei, nativeTokenPriceUsdt) {
162
+ const priceScale = 10n ** 18n;
163
+ const scaledPrice = BigInt(Math.round(nativeTokenPriceUsdt * Number(priceScale)));
164
+ const usdtBase = feeInWei * scaledPrice * 1000000n / (10n ** 18n * priceScale);
165
+ const whole = usdtBase / 1000000n;
166
+ const frac = (usdtBase % 1000000n).toString().padStart(6, "0");
167
+ return `${whole}.${frac}`;
168
+ }
169
+
170
+ // src/evmERC4337TetherTransfer.ts
171
+ var TetherEVMERC4337Transfer = class {
172
+ /** Default timeout for UserOp confirmation: 120 seconds. */
173
+ static USER_OP_TIMEOUT_MS = 12e4;
174
+ /**
175
+ * Wait for a UserOperation to be included on-chain via the Candide bundler.
176
+ * Times out after USER_OP_TIMEOUT_MS to prevent indefinite hangs.
177
+ */
178
+ static async waitForUserOpConfirmation(userOpHash, bundlerUrl, entryPointAddress) {
179
+ const bundler = new import_abstractionkit.Bundler(bundlerUrl);
180
+ const response = new import_abstractionkit.SendUseroperationResponse(userOpHash, bundler, entryPointAddress);
181
+ const timeoutPromise = new Promise((_, reject) => {
182
+ setTimeout(
183
+ () => reject(
184
+ new import_core2.TransactionFailedError(
185
+ "evm",
186
+ `UserOperation confirmation timed out after ${this.USER_OP_TIMEOUT_MS / 1e3}s. Hash: ${userOpHash}`
187
+ )
188
+ ),
189
+ this.USER_OP_TIMEOUT_MS
190
+ );
191
+ });
192
+ const receipt = await Promise.race([response.included(), timeoutPromise]);
193
+ if (!receipt.success) {
194
+ throw new import_core2.TransactionFailedError(
195
+ "evm",
196
+ `UserOperation reverted. Tx: ${receipt.receipt.transactionHash}`
197
+ );
198
+ }
199
+ return receipt.receipt.transactionHash;
200
+ }
201
+ /**
202
+ * Get transaction fee estimate for a USDT transfer.
203
+ *
204
+ * Uses a minimal quote amount (gas cost is the same regardless of transfer
205
+ * amount). Applies a gas buffer (35% Ethereum, 30% others) for volatility.
206
+ */
207
+ static async getTransactionEstimateFee(seedPhrase, config, recipientAddress, accountIndex = 0) {
208
+ (0, import_core2.validateEvmAddress)(recipientAddress, config.chain, "recipient address");
209
+ const networkConfig = getErc4337ConfigForChain(config);
210
+ const { wallet, account } = await setupErc4337Wallet(seedPhrase, config, accountIndex);
211
+ const tokenAddress = networkConfig.paymasterToken.address;
212
+ try {
213
+ const quoteAmount = toUsdtBaseUnitsEvm("0.01");
214
+ let feeVal;
215
+ try {
216
+ const transferQuote = await account.quoteTransfer({
217
+ token: tokenAddress,
218
+ recipient: recipientAddress,
219
+ amount: quoteAmount
220
+ });
221
+ const bufferPercent = config.chain === "ethereum" ? 35n : 30n;
222
+ const bufferedFee = transferQuote.fee + transferQuote.fee * bufferPercent / 100n;
223
+ feeVal = formatTokenBalance(bufferedFee, 6);
224
+ } catch (quoteError) {
225
+ if (quoteError instanceof Error && (quoteError.message.includes("token allowance lower than the required") || quoteError.message.includes("token balance lower than the required"))) {
226
+ const estimates = {
227
+ ethereum: "1.40",
228
+ arbitrum: "0.10",
229
+ polygon: "0.10",
230
+ celo: "0.05",
231
+ optimism: "0.10",
232
+ base: "0.15"
233
+ };
234
+ feeVal = estimates[config.chain] || "0.15";
235
+ } else {
236
+ throw quoteError;
237
+ }
238
+ }
239
+ return {
240
+ message: "Transaction fee estimate retrieved successfully",
241
+ success: true,
242
+ data: { fee: feeVal }
243
+ };
244
+ } finally {
245
+ account.dispose();
246
+ wallet.dispose();
247
+ }
248
+ }
249
+ /**
250
+ * Check token balance on an EVM chain.
251
+ */
252
+ static async checkTokenBalance(seedPhrase, config, tokenAddress) {
253
+ (0, import_core2.validateEvmAddress)(tokenAddress, config.chain, "token address");
254
+ const { wallet, account } = await setupErc4337Wallet(seedPhrase, config);
255
+ try {
256
+ const balance = await account.getTokenBalance(tokenAddress);
257
+ const usdBalance = formatTokenBalance(balance, 6);
258
+ return {
259
+ message: `${config.chain.toUpperCase()} balances retrieved successfully`,
260
+ success: true,
261
+ data: {
262
+ tokenBalance: balance.toString(),
263
+ decimals: 6,
264
+ usdBalance: String(usdBalance)
265
+ }
266
+ };
267
+ } finally {
268
+ account.dispose();
269
+ wallet.dispose();
270
+ }
271
+ }
272
+ /**
273
+ * Send a single USDT transfer via ERC-4337.
274
+ *
275
+ * Handles:
276
+ * - Balance validation
277
+ * - Gas fee auto-adjustment for non-sponsored mode
278
+ * - On-chain confirmation via Candide bundler
279
+ * - User-friendly error mapping
280
+ */
281
+ static async sendToken(seedPhrase, config, tokenAddress, amount, recipientAddress, accountIndex = 0) {
282
+ const networkConfig = getErc4337ConfigForChain(config);
283
+ const gasSponsored = networkConfig.isSponsored;
284
+ (0, import_core2.validateEvmAddress)(tokenAddress, config.chain, "token address");
285
+ (0, import_core2.validateEvmAddress)(recipientAddress, config.chain, "recipient address");
286
+ const {
287
+ wallet,
288
+ account,
289
+ address: senderAddress
290
+ } = await setupErc4337Wallet(seedPhrase, config, accountIndex);
291
+ try {
292
+ if (recipientAddress.toLowerCase() === senderAddress.toLowerCase()) {
293
+ throw new import_core2.TransactionFailedError(config.chain, "Cannot transfer to your own address");
294
+ }
295
+ const amountInBaseUnits = toUsdtBaseUnitsEvm(amount);
296
+ const balance = await account.getTokenBalance(tokenAddress);
297
+ if (!gasSponsored) {
298
+ let gasFeeBaseUnits;
299
+ try {
300
+ const quote = await account.quoteTransfer({
301
+ token: tokenAddress,
302
+ recipient: recipientAddress,
303
+ amount: amountInBaseUnits
304
+ });
305
+ const bufferPercent = config.chain === "ethereum" ? 35n : 30n;
306
+ gasFeeBaseUnits = quote.fee + quote.fee * bufferPercent / 100n;
307
+ } catch {
308
+ gasFeeBaseUnits = GAS_FEE_FALLBACKS[config.chain] ?? 150000n;
309
+ }
310
+ const totalRequired = amountInBaseUnits + gasFeeBaseUnits;
311
+ if (BigInt(balance) < totalRequired) {
312
+ throw new import_core2.InsufficientBalanceError(
313
+ config.chain,
314
+ `USDT (need ${formatTokenBalance(totalRequired, 6)} but have ${formatTokenBalance(balance, 6)} \u2014 includes ${formatTokenBalance(gasFeeBaseUnits, 6)} gas fee)`
315
+ );
316
+ }
317
+ } else if (BigInt(balance) < amountInBaseUnits) {
318
+ throw new import_core2.InsufficientBalanceError(config.chain, "USDT");
319
+ }
320
+ const transferResult = await account.transfer({
321
+ token: tokenAddress,
322
+ recipient: recipientAddress,
323
+ amount: amountInBaseUnits
324
+ });
325
+ const transactionHash = await this.waitForUserOpConfirmation(
326
+ transferResult.hash,
327
+ networkConfig.bundlerUrl,
328
+ networkConfig.entryPointAddress
329
+ );
330
+ return {
331
+ success: true,
332
+ transactionHash,
333
+ chain: config.chain,
334
+ amount
335
+ };
336
+ } catch (error) {
337
+ throw this.handleTransferError(error, config.chain);
338
+ } finally {
339
+ account.dispose();
340
+ wallet.dispose();
341
+ }
342
+ }
343
+ /**
344
+ * Send a batch USDT transfer to multiple recipients in a single UserOperation.
345
+ */
346
+ static async sendBatchToken(seedPhrase, config, tokenAddress, recipients, accountIndex = 0) {
347
+ const networkConfig = getErc4337ConfigForChain(config);
348
+ const gasSponsored = networkConfig.isSponsored;
349
+ for (const r of recipients) {
350
+ (0, import_core2.validateEvmAddress)(r.address, config.chain, "recipient address");
351
+ }
352
+ const {
353
+ wallet,
354
+ account,
355
+ address: senderAddress
356
+ } = await setupErc4337Wallet(seedPhrase, config, accountIndex);
357
+ try {
358
+ for (const r of recipients) {
359
+ if (r.address.toLowerCase() === senderAddress.toLowerCase()) {
360
+ throw new import_core2.TransactionFailedError(config.chain, "Cannot transfer to your own address");
361
+ }
362
+ }
363
+ const totalAmount = recipients.reduce((sum, r) => sum + toUsdtBaseUnitsEvm(r.amount), 0n);
364
+ const balance = await account.getTokenBalance(tokenAddress);
365
+ if (!gasSponsored) {
366
+ const gasFeeReserve = GAS_FEE_FALLBACKS[config.chain] ?? 150000n;
367
+ const totalRequired = totalAmount + gasFeeReserve;
368
+ if (BigInt(balance) < totalRequired) {
369
+ throw new import_core2.InsufficientBalanceError(
370
+ config.chain,
371
+ `USDT (need ${formatTokenBalance(totalRequired, 6)} but have ${formatTokenBalance(balance, 6)} \u2014 includes ${formatTokenBalance(gasFeeReserve, 6)} gas fee)`
372
+ );
373
+ }
374
+ } else if (BigInt(balance) < totalAmount) {
375
+ throw new import_core2.InsufficientBalanceError(config.chain, "USDT");
376
+ }
377
+ const batchTransactions = recipients.map(({ address, amount }) => {
378
+ const amountBase = toUsdtBaseUnitsEvm(amount);
379
+ return {
380
+ to: tokenAddress,
381
+ value: 0,
382
+ data: encodeErc20Transfer(address, amountBase)
383
+ };
384
+ });
385
+ const transferResult = await account.sendTransaction(batchTransactions);
386
+ const transactionHash = await this.waitForUserOpConfirmation(
387
+ transferResult.hash,
388
+ networkConfig.bundlerUrl,
389
+ networkConfig.entryPointAddress
390
+ );
391
+ return {
392
+ success: true,
393
+ transactionHash,
394
+ chain: config.chain,
395
+ recipients
396
+ };
397
+ } catch (error) {
398
+ throw this.handleTransferError(error, config.chain);
399
+ } finally {
400
+ account.dispose();
401
+ wallet.dispose();
402
+ }
403
+ }
404
+ /**
405
+ * Map raw errors into user-friendly messages.
406
+ */
407
+ static handleTransferError(error, chain) {
408
+ if (!(error instanceof Error)) {
409
+ return new import_core2.TransactionFailedError(chain, "Unknown error");
410
+ }
411
+ const msg = error.message;
412
+ if (msg.includes("callData reverts")) {
413
+ return new import_core2.TransactionFailedError(
414
+ chain,
415
+ "Transaction reverted. Check your balance and try again."
416
+ );
417
+ }
418
+ if (msg.includes("not enough funds")) {
419
+ return new import_core2.InsufficientBalanceError(chain, "paymaster token");
420
+ }
421
+ if (msg.includes("token balance lower than the required") || msg.includes("token allowance lower than the required")) {
422
+ return new import_core2.InsufficientBalanceError(chain, "USDT (insufficient for transfer + gas)");
423
+ }
424
+ if (msg.includes("Insufficient") || error instanceof import_core2.InsufficientBalanceError) {
425
+ return error;
426
+ }
427
+ if (msg.includes("nonce too low")) {
428
+ return new import_core2.TransactionFailedError(chain, "Nonce conflict. Please try again.");
429
+ }
430
+ if (msg.includes("insufficient funds") || msg.includes("transfer amount exceeds balance")) {
431
+ return new import_core2.InsufficientBalanceError(chain, "USDT");
432
+ }
433
+ if (msg.includes("transaction underpriced") || msg.includes("max fee per gas less than block base fee")) {
434
+ return new import_core2.TransactionFailedError(chain, "Gas price too low. Please try again.");
435
+ }
436
+ if (msg.includes("out of gas") || msg.includes("intrinsic gas too low")) {
437
+ return new import_core2.TransactionFailedError(chain, "Insufficient gas limit.");
438
+ }
439
+ if (msg.includes("execution reverted")) {
440
+ return new import_core2.TransactionFailedError(chain, "Transaction reverted on-chain.");
441
+ }
442
+ if (msg.includes("504") || msg.includes("Gateway Time-out")) {
443
+ return new import_core2.TransactionFailedError(chain, "Network timeout. Please try again.");
444
+ }
445
+ return new import_core2.TransactionFailedError(chain, msg);
446
+ }
447
+ };
448
+ // Annotate the CommonJS export names for ESM import in node:
449
+ 0 && (module.exports = {
450
+ GAS_FEE_ESTIMATES,
451
+ GAS_FEE_FALLBACKS,
452
+ TetherEVMERC4337Transfer,
453
+ encodeErc20Transfer,
454
+ feeToUsdt,
455
+ formatTokenBalance,
456
+ getErc4337ConfigForChain,
457
+ setupErc4337Wallet,
458
+ toUsdtBaseUnitsEvm
459
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,418 @@
1
+ // src/evmNetworkConfiguration.ts
2
+ import { EVM_CHAINS } from "@gasfree-kit/core";
3
+ var DEFAULT_ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
4
+ var DEFAULT_SAFE_MODULES_VERSION = "0.3.0";
5
+ var DEFAULT_PAYMASTER_ADDRESS = "0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba";
6
+ var DEFAULT_TRANSFER_MAX_FEE = 1e15;
7
+ function getErc4337ConfigForChain(config) {
8
+ const chainConfig = EVM_CHAINS[config.chain];
9
+ return {
10
+ chainId: chainConfig.chainId,
11
+ provider: config.rpcUrl,
12
+ bundlerUrl: config.bundlerUrl,
13
+ paymasterUrl: config.paymasterUrl,
14
+ paymasterAddress: config.paymasterAddress ?? DEFAULT_PAYMASTER_ADDRESS,
15
+ entryPointAddress: config.entryPointAddress ?? DEFAULT_ENTRY_POINT,
16
+ safeModulesVersion: config.safeModulesVersion ?? DEFAULT_SAFE_MODULES_VERSION,
17
+ transferMaxFee: DEFAULT_TRANSFER_MAX_FEE,
18
+ isSponsored: config.isSponsored,
19
+ sponsorshipPolicyId: config.sponsorshipPolicyId ?? "",
20
+ paymasterToken: {
21
+ address: config.paymasterTokenAddress ?? chainConfig.usdtAddress
22
+ }
23
+ };
24
+ }
25
+ var GAS_FEE_FALLBACKS = {
26
+ ethereum: 1400000n,
27
+ arbitrum: 100000n,
28
+ polygon: 100000n,
29
+ celo: 50000n,
30
+ optimism: 100000n,
31
+ base: 150000n
32
+ };
33
+ var GAS_FEE_ESTIMATES = {
34
+ ethereum: "1.40",
35
+ arbitrum: "0.10",
36
+ polygon: "0.10",
37
+ celo: "0.05",
38
+ optimism: "0.10",
39
+ base: "0.15"
40
+ };
41
+
42
+ // src/evmERC4337.ts
43
+ async function setupErc4337Wallet(seedPhrase, config, accountIndex = 0) {
44
+ const networkConfig = getErc4337ConfigForChain(config);
45
+ const WalletManagerEvmErc4337Module = await import("@tetherto/wdk-wallet-evm-erc-4337");
46
+ const WalletManagerEvmErc4337 = WalletManagerEvmErc4337Module.default;
47
+ const wdkConfig = networkConfig.isSponsored ? {
48
+ chainId: networkConfig.chainId,
49
+ provider: networkConfig.provider,
50
+ bundlerUrl: networkConfig.bundlerUrl,
51
+ entryPointAddress: networkConfig.entryPointAddress,
52
+ safeModulesVersion: networkConfig.safeModulesVersion,
53
+ isSponsored: true,
54
+ paymasterUrl: networkConfig.paymasterUrl,
55
+ sponsorshipPolicyId: networkConfig.sponsorshipPolicyId
56
+ } : {
57
+ chainId: networkConfig.chainId,
58
+ provider: networkConfig.provider,
59
+ bundlerUrl: networkConfig.bundlerUrl,
60
+ entryPointAddress: networkConfig.entryPointAddress,
61
+ safeModulesVersion: networkConfig.safeModulesVersion,
62
+ paymasterUrl: networkConfig.paymasterUrl,
63
+ paymasterAddress: networkConfig.paymasterAddress,
64
+ paymasterToken: networkConfig.paymasterToken
65
+ };
66
+ const wallet = new WalletManagerEvmErc4337(
67
+ seedPhrase,
68
+ wdkConfig
69
+ );
70
+ const account = await wallet.getAccount(accountIndex);
71
+ const address = await account.getAddress();
72
+ return { wallet, account, address };
73
+ }
74
+
75
+ // src/evmERC4337TetherTransfer.ts
76
+ import { Bundler, SendUseroperationResponse } from "abstractionkit";
77
+ import {
78
+ InsufficientBalanceError,
79
+ TransactionFailedError,
80
+ validateEvmAddress
81
+ } from "@gasfree-kit/core";
82
+
83
+ // src/evmUtility.ts
84
+ function toUsdtBaseUnitsEvm(amount) {
85
+ const str = amount.toString().trim();
86
+ if (!/^\d+(\.\d+)?$/.test(str)) {
87
+ throw new Error(`Invalid USDT amount: "${str}". Must be a non-negative decimal number.`);
88
+ }
89
+ const decimals = 6;
90
+ const [integer, fraction = ""] = str.split(".");
91
+ if (fraction.length > decimals) {
92
+ throw new Error(
93
+ `USDT amount has too many decimal places (${fraction.length}). Maximum is ${decimals}.`
94
+ );
95
+ }
96
+ const normalizedFraction = (fraction + "0".repeat(decimals)).slice(0, decimals);
97
+ return BigInt(integer + normalizedFraction);
98
+ }
99
+ function formatTokenBalance(rawBalance, decimals = 18, precision = 6) {
100
+ const divisor = 10n ** BigInt(decimals);
101
+ const whole = rawBalance / divisor;
102
+ const fraction = rawBalance % divisor;
103
+ const fractionStr = fraction.toString().padStart(decimals, "0").slice(0, precision);
104
+ const result = `${whole}.${fractionStr}`.replace(/\.?0+$/, "");
105
+ return result || "0";
106
+ }
107
+ function encodeErc20Transfer(to, amount) {
108
+ const TRANSFER_SELECTOR = "0xa9059cbb";
109
+ const addr = to.startsWith("0x") ? to.slice(2) : to;
110
+ if (addr.length !== 40) {
111
+ throw new Error(`Invalid address length for ABI encoding: ${to}`);
112
+ }
113
+ const amountHex = amount.toString(16);
114
+ if (amountHex.length > 64) {
115
+ throw new Error(`Amount too large for uint256 encoding: ${amount}`);
116
+ }
117
+ const paddedAddress = addr.toLowerCase().padStart(64, "0");
118
+ const paddedAmount = amountHex.padStart(64, "0");
119
+ return `${TRANSFER_SELECTOR}${paddedAddress}${paddedAmount}`;
120
+ }
121
+ function feeToUsdt(feeInWei, nativeTokenPriceUsdt) {
122
+ const priceScale = 10n ** 18n;
123
+ const scaledPrice = BigInt(Math.round(nativeTokenPriceUsdt * Number(priceScale)));
124
+ const usdtBase = feeInWei * scaledPrice * 1000000n / (10n ** 18n * priceScale);
125
+ const whole = usdtBase / 1000000n;
126
+ const frac = (usdtBase % 1000000n).toString().padStart(6, "0");
127
+ return `${whole}.${frac}`;
128
+ }
129
+
130
+ // src/evmERC4337TetherTransfer.ts
131
+ var TetherEVMERC4337Transfer = class {
132
+ /** Default timeout for UserOp confirmation: 120 seconds. */
133
+ static USER_OP_TIMEOUT_MS = 12e4;
134
+ /**
135
+ * Wait for a UserOperation to be included on-chain via the Candide bundler.
136
+ * Times out after USER_OP_TIMEOUT_MS to prevent indefinite hangs.
137
+ */
138
+ static async waitForUserOpConfirmation(userOpHash, bundlerUrl, entryPointAddress) {
139
+ const bundler = new Bundler(bundlerUrl);
140
+ const response = new SendUseroperationResponse(userOpHash, bundler, entryPointAddress);
141
+ const timeoutPromise = new Promise((_, reject) => {
142
+ setTimeout(
143
+ () => reject(
144
+ new TransactionFailedError(
145
+ "evm",
146
+ `UserOperation confirmation timed out after ${this.USER_OP_TIMEOUT_MS / 1e3}s. Hash: ${userOpHash}`
147
+ )
148
+ ),
149
+ this.USER_OP_TIMEOUT_MS
150
+ );
151
+ });
152
+ const receipt = await Promise.race([response.included(), timeoutPromise]);
153
+ if (!receipt.success) {
154
+ throw new TransactionFailedError(
155
+ "evm",
156
+ `UserOperation reverted. Tx: ${receipt.receipt.transactionHash}`
157
+ );
158
+ }
159
+ return receipt.receipt.transactionHash;
160
+ }
161
+ /**
162
+ * Get transaction fee estimate for a USDT transfer.
163
+ *
164
+ * Uses a minimal quote amount (gas cost is the same regardless of transfer
165
+ * amount). Applies a gas buffer (35% Ethereum, 30% others) for volatility.
166
+ */
167
+ static async getTransactionEstimateFee(seedPhrase, config, recipientAddress, accountIndex = 0) {
168
+ validateEvmAddress(recipientAddress, config.chain, "recipient address");
169
+ const networkConfig = getErc4337ConfigForChain(config);
170
+ const { wallet, account } = await setupErc4337Wallet(seedPhrase, config, accountIndex);
171
+ const tokenAddress = networkConfig.paymasterToken.address;
172
+ try {
173
+ const quoteAmount = toUsdtBaseUnitsEvm("0.01");
174
+ let feeVal;
175
+ try {
176
+ const transferQuote = await account.quoteTransfer({
177
+ token: tokenAddress,
178
+ recipient: recipientAddress,
179
+ amount: quoteAmount
180
+ });
181
+ const bufferPercent = config.chain === "ethereum" ? 35n : 30n;
182
+ const bufferedFee = transferQuote.fee + transferQuote.fee * bufferPercent / 100n;
183
+ feeVal = formatTokenBalance(bufferedFee, 6);
184
+ } catch (quoteError) {
185
+ if (quoteError instanceof Error && (quoteError.message.includes("token allowance lower than the required") || quoteError.message.includes("token balance lower than the required"))) {
186
+ const estimates = {
187
+ ethereum: "1.40",
188
+ arbitrum: "0.10",
189
+ polygon: "0.10",
190
+ celo: "0.05",
191
+ optimism: "0.10",
192
+ base: "0.15"
193
+ };
194
+ feeVal = estimates[config.chain] || "0.15";
195
+ } else {
196
+ throw quoteError;
197
+ }
198
+ }
199
+ return {
200
+ message: "Transaction fee estimate retrieved successfully",
201
+ success: true,
202
+ data: { fee: feeVal }
203
+ };
204
+ } finally {
205
+ account.dispose();
206
+ wallet.dispose();
207
+ }
208
+ }
209
+ /**
210
+ * Check token balance on an EVM chain.
211
+ */
212
+ static async checkTokenBalance(seedPhrase, config, tokenAddress) {
213
+ validateEvmAddress(tokenAddress, config.chain, "token address");
214
+ const { wallet, account } = await setupErc4337Wallet(seedPhrase, config);
215
+ try {
216
+ const balance = await account.getTokenBalance(tokenAddress);
217
+ const usdBalance = formatTokenBalance(balance, 6);
218
+ return {
219
+ message: `${config.chain.toUpperCase()} balances retrieved successfully`,
220
+ success: true,
221
+ data: {
222
+ tokenBalance: balance.toString(),
223
+ decimals: 6,
224
+ usdBalance: String(usdBalance)
225
+ }
226
+ };
227
+ } finally {
228
+ account.dispose();
229
+ wallet.dispose();
230
+ }
231
+ }
232
+ /**
233
+ * Send a single USDT transfer via ERC-4337.
234
+ *
235
+ * Handles:
236
+ * - Balance validation
237
+ * - Gas fee auto-adjustment for non-sponsored mode
238
+ * - On-chain confirmation via Candide bundler
239
+ * - User-friendly error mapping
240
+ */
241
+ static async sendToken(seedPhrase, config, tokenAddress, amount, recipientAddress, accountIndex = 0) {
242
+ const networkConfig = getErc4337ConfigForChain(config);
243
+ const gasSponsored = networkConfig.isSponsored;
244
+ validateEvmAddress(tokenAddress, config.chain, "token address");
245
+ validateEvmAddress(recipientAddress, config.chain, "recipient address");
246
+ const {
247
+ wallet,
248
+ account,
249
+ address: senderAddress
250
+ } = await setupErc4337Wallet(seedPhrase, config, accountIndex);
251
+ try {
252
+ if (recipientAddress.toLowerCase() === senderAddress.toLowerCase()) {
253
+ throw new TransactionFailedError(config.chain, "Cannot transfer to your own address");
254
+ }
255
+ const amountInBaseUnits = toUsdtBaseUnitsEvm(amount);
256
+ const balance = await account.getTokenBalance(tokenAddress);
257
+ if (!gasSponsored) {
258
+ let gasFeeBaseUnits;
259
+ try {
260
+ const quote = await account.quoteTransfer({
261
+ token: tokenAddress,
262
+ recipient: recipientAddress,
263
+ amount: amountInBaseUnits
264
+ });
265
+ const bufferPercent = config.chain === "ethereum" ? 35n : 30n;
266
+ gasFeeBaseUnits = quote.fee + quote.fee * bufferPercent / 100n;
267
+ } catch {
268
+ gasFeeBaseUnits = GAS_FEE_FALLBACKS[config.chain] ?? 150000n;
269
+ }
270
+ const totalRequired = amountInBaseUnits + gasFeeBaseUnits;
271
+ if (BigInt(balance) < totalRequired) {
272
+ throw new InsufficientBalanceError(
273
+ config.chain,
274
+ `USDT (need ${formatTokenBalance(totalRequired, 6)} but have ${formatTokenBalance(balance, 6)} \u2014 includes ${formatTokenBalance(gasFeeBaseUnits, 6)} gas fee)`
275
+ );
276
+ }
277
+ } else if (BigInt(balance) < amountInBaseUnits) {
278
+ throw new InsufficientBalanceError(config.chain, "USDT");
279
+ }
280
+ const transferResult = await account.transfer({
281
+ token: tokenAddress,
282
+ recipient: recipientAddress,
283
+ amount: amountInBaseUnits
284
+ });
285
+ const transactionHash = await this.waitForUserOpConfirmation(
286
+ transferResult.hash,
287
+ networkConfig.bundlerUrl,
288
+ networkConfig.entryPointAddress
289
+ );
290
+ return {
291
+ success: true,
292
+ transactionHash,
293
+ chain: config.chain,
294
+ amount
295
+ };
296
+ } catch (error) {
297
+ throw this.handleTransferError(error, config.chain);
298
+ } finally {
299
+ account.dispose();
300
+ wallet.dispose();
301
+ }
302
+ }
303
+ /**
304
+ * Send a batch USDT transfer to multiple recipients in a single UserOperation.
305
+ */
306
+ static async sendBatchToken(seedPhrase, config, tokenAddress, recipients, accountIndex = 0) {
307
+ const networkConfig = getErc4337ConfigForChain(config);
308
+ const gasSponsored = networkConfig.isSponsored;
309
+ for (const r of recipients) {
310
+ validateEvmAddress(r.address, config.chain, "recipient address");
311
+ }
312
+ const {
313
+ wallet,
314
+ account,
315
+ address: senderAddress
316
+ } = await setupErc4337Wallet(seedPhrase, config, accountIndex);
317
+ try {
318
+ for (const r of recipients) {
319
+ if (r.address.toLowerCase() === senderAddress.toLowerCase()) {
320
+ throw new TransactionFailedError(config.chain, "Cannot transfer to your own address");
321
+ }
322
+ }
323
+ const totalAmount = recipients.reduce((sum, r) => sum + toUsdtBaseUnitsEvm(r.amount), 0n);
324
+ const balance = await account.getTokenBalance(tokenAddress);
325
+ if (!gasSponsored) {
326
+ const gasFeeReserve = GAS_FEE_FALLBACKS[config.chain] ?? 150000n;
327
+ const totalRequired = totalAmount + gasFeeReserve;
328
+ if (BigInt(balance) < totalRequired) {
329
+ throw new InsufficientBalanceError(
330
+ config.chain,
331
+ `USDT (need ${formatTokenBalance(totalRequired, 6)} but have ${formatTokenBalance(balance, 6)} \u2014 includes ${formatTokenBalance(gasFeeReserve, 6)} gas fee)`
332
+ );
333
+ }
334
+ } else if (BigInt(balance) < totalAmount) {
335
+ throw new InsufficientBalanceError(config.chain, "USDT");
336
+ }
337
+ const batchTransactions = recipients.map(({ address, amount }) => {
338
+ const amountBase = toUsdtBaseUnitsEvm(amount);
339
+ return {
340
+ to: tokenAddress,
341
+ value: 0,
342
+ data: encodeErc20Transfer(address, amountBase)
343
+ };
344
+ });
345
+ const transferResult = await account.sendTransaction(batchTransactions);
346
+ const transactionHash = await this.waitForUserOpConfirmation(
347
+ transferResult.hash,
348
+ networkConfig.bundlerUrl,
349
+ networkConfig.entryPointAddress
350
+ );
351
+ return {
352
+ success: true,
353
+ transactionHash,
354
+ chain: config.chain,
355
+ recipients
356
+ };
357
+ } catch (error) {
358
+ throw this.handleTransferError(error, config.chain);
359
+ } finally {
360
+ account.dispose();
361
+ wallet.dispose();
362
+ }
363
+ }
364
+ /**
365
+ * Map raw errors into user-friendly messages.
366
+ */
367
+ static handleTransferError(error, chain) {
368
+ if (!(error instanceof Error)) {
369
+ return new TransactionFailedError(chain, "Unknown error");
370
+ }
371
+ const msg = error.message;
372
+ if (msg.includes("callData reverts")) {
373
+ return new TransactionFailedError(
374
+ chain,
375
+ "Transaction reverted. Check your balance and try again."
376
+ );
377
+ }
378
+ if (msg.includes("not enough funds")) {
379
+ return new InsufficientBalanceError(chain, "paymaster token");
380
+ }
381
+ if (msg.includes("token balance lower than the required") || msg.includes("token allowance lower than the required")) {
382
+ return new InsufficientBalanceError(chain, "USDT (insufficient for transfer + gas)");
383
+ }
384
+ if (msg.includes("Insufficient") || error instanceof InsufficientBalanceError) {
385
+ return error;
386
+ }
387
+ if (msg.includes("nonce too low")) {
388
+ return new TransactionFailedError(chain, "Nonce conflict. Please try again.");
389
+ }
390
+ if (msg.includes("insufficient funds") || msg.includes("transfer amount exceeds balance")) {
391
+ return new InsufficientBalanceError(chain, "USDT");
392
+ }
393
+ if (msg.includes("transaction underpriced") || msg.includes("max fee per gas less than block base fee")) {
394
+ return new TransactionFailedError(chain, "Gas price too low. Please try again.");
395
+ }
396
+ if (msg.includes("out of gas") || msg.includes("intrinsic gas too low")) {
397
+ return new TransactionFailedError(chain, "Insufficient gas limit.");
398
+ }
399
+ if (msg.includes("execution reverted")) {
400
+ return new TransactionFailedError(chain, "Transaction reverted on-chain.");
401
+ }
402
+ if (msg.includes("504") || msg.includes("Gateway Time-out")) {
403
+ return new TransactionFailedError(chain, "Network timeout. Please try again.");
404
+ }
405
+ return new TransactionFailedError(chain, msg);
406
+ }
407
+ };
408
+ export {
409
+ GAS_FEE_ESTIMATES,
410
+ GAS_FEE_FALLBACKS,
411
+ TetherEVMERC4337Transfer,
412
+ encodeErc20Transfer,
413
+ feeToUsdt,
414
+ formatTokenBalance,
415
+ getErc4337ConfigForChain,
416
+ setupErc4337Wallet,
417
+ toUsdtBaseUnitsEvm
418
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@gasfree-kit/evm-4337",
3
+ "version": "0.1.0",
4
+ "description": "ERC-4337 gasless transactions for EVM chains — powered by WDK",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "patches",
18
+ "scripts"
19
+ ],
20
+ "dependencies": {
21
+ "abstractionkit": "^0.2.30",
22
+ "@gasfree-kit/core": "0.1.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@tetherto/wdk-wallet-evm-erc-4337": "^1.0.0-beta.5"
26
+ },
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "postinstall": "node scripts/postinstall.js",
33
+ "build": "tsup",
34
+ "dev": "tsup --watch",
35
+ "test": "vitest run",
36
+ "typecheck": "tsc --noEmit",
37
+ "clean": "rm -rf dist"
38
+ }
39
+ }
@@ -0,0 +1,68 @@
1
+ diff --git a/dist/cjs/src/index.cjs b/dist/cjs/src/index.cjs
2
+ --- a/dist/cjs/src/index.cjs
3
+ +++ b/dist/cjs/src/index.cjs
4
+ @@ -1946,6 +1946,9 @@ var GenericFeeEstimator = class {
5
+ const context = "paymasterTokenAddress" in paymasterOptions ? {
6
+ token: paymasterOptions.paymasterTokenAddress
7
+ } : paymasterOptions.paymasterContext ?? {};
8
+ + if (paymasterOptions.sponsorshipPolicyId) {
9
+ + context.sponsorshipPolicyId = paymasterOptions.sponsorshipPolicyId;
10
+ + }
11
+ const [feeData, paymasterStubData] = await Promise.all([
12
+ this.#getUserOperationGasPrices(this.rpcUrl),
13
+ paymasterClient.request({
14
+ @@ -2017,7 +2020,8 @@ var GenericFeeEstimator = class {
15
+ const sponsoredData = await paymasterClient.request({
16
+ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */,
17
+ params
18
+ });
19
+ - return sponsoredData;
20
+ + sponsoredData.callGasLimit = userOperation.callGasLimit.toString();
21
+ + return sponsoredData;
22
+ }
23
+ const erc20PaymasterData = await paymasterClient.request({
24
+ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */,
25
+ @@ -2032,7 +2036,8 @@ var GenericFeeEstimator = class {
26
+ const threshold = await protocolKit.getThreshold();
27
+ erc20PaymasterData.verificationGasLimit = (BigInt(erc20PaymasterData.verificationGasLimit) + BigInt(threshold) * this.defaultVerificationGasLimitOverhead).toString();
28
+ }
29
+ - return erc20PaymasterData;
30
+ + erc20PaymasterData.callGasLimit = userOperation.callGasLimit.toString();
31
+ + return erc20PaymasterData;
32
+ }
33
+ async #getUserOperationGasPrices(rpcUrl) {
34
+ const client = (0, import_viem11.createPublicClient)({
35
+ diff --git a/dist/esm/src/index.mjs b/dist/esm/src/index.mjs
36
+ --- a/dist/esm/src/index.mjs
37
+ +++ b/dist/esm/src/index.mjs
38
+ @@ -1923,6 +1923,9 @@ var GenericFeeEstimator = class {
39
+ const context = "paymasterTokenAddress" in paymasterOptions ? {
40
+ token: paymasterOptions.paymasterTokenAddress
41
+ } : paymasterOptions.paymasterContext ?? {};
42
+ + if (paymasterOptions.sponsorshipPolicyId) {
43
+ + context.sponsorshipPolicyId = paymasterOptions.sponsorshipPolicyId;
44
+ + }
45
+ const [feeData, paymasterStubData] = await Promise.all([
46
+ this.#getUserOperationGasPrices(this.rpcUrl),
47
+ paymasterClient.request({
48
+ @@ -1994,7 +1997,8 @@ var GenericFeeEstimator = class {
49
+ const sponsoredData = await paymasterClient.request({
50
+ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */,
51
+ params
52
+ });
53
+ - return sponsoredData;
54
+ + sponsoredData.callGasLimit = userOperation.callGasLimit.toString();
55
+ + return sponsoredData;
56
+ }
57
+ const erc20PaymasterData = await paymasterClient.request({
58
+ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */,
59
+ @@ -2009,7 +2013,8 @@ var GenericFeeEstimator = class {
60
+ const threshold = await protocolKit.getThreshold();
61
+ erc20PaymasterData.verificationGasLimit = (BigInt(erc20PaymasterData.verificationGasLimit) + BigInt(threshold) * this.defaultVerificationGasLimitOverhead).toString();
62
+ }
63
+ - return erc20PaymasterData;
64
+ + erc20PaymasterData.callGasLimit = userOperation.callGasLimit.toString();
65
+ + return erc20PaymasterData;
66
+ }
67
+ async #getUserOperationGasPrices(rpcUrl) {
68
+ const client = createPublicClient2({
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for @gasfree-kit/evm-4337.
5
+ *
6
+ * Applies a critical patch to @tetherto/wdk-safe-relay-kit that fixes:
7
+ * 1. sponsorshipPolicyId not being passed to Candide paymaster context
8
+ * 2. callGasLimit being dropped by paymaster response (both sponsored & ERC-20)
9
+ *
10
+ * Without this patch, gas estimation fails on Candide-sponsored transactions.
11
+ *
12
+ * Works with npm, pnpm, and yarn.
13
+ * pnpm users can also use patchedDependencies in package.json instead.
14
+ */
15
+
16
+ const { execSync } = require('child_process');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const patchFile = path.join(
21
+ __dirname,
22
+ '..',
23
+ 'patches',
24
+ '@tetherto__wdk-safe-relay-kit@4.1.5.patch',
25
+ );
26
+
27
+ if (!fs.existsSync(patchFile)) {
28
+ // Patch file not found — skip (might be in a CI environment or already applied)
29
+ process.exit(0);
30
+ }
31
+
32
+ // Find the target package
33
+ const possiblePaths = [
34
+ // npm/yarn hoisted
35
+ path.resolve(__dirname, '..', '..', '@tetherto', 'wdk-safe-relay-kit'),
36
+ // pnpm non-hoisted
37
+ path.resolve(__dirname, '..', 'node_modules', '@tetherto', 'wdk-safe-relay-kit'),
38
+ // monorepo root
39
+ path.resolve(__dirname, '..', '..', '..', 'node_modules', '@tetherto', 'wdk-safe-relay-kit'),
40
+ ];
41
+
42
+ const targetDir = possiblePaths.find((p) => fs.existsSync(p));
43
+
44
+ if (!targetDir) {
45
+ // @tetherto/wdk-safe-relay-kit not installed yet — skip
46
+ // The user's package manager will install peer deps and this runs again
47
+ process.exit(0);
48
+ }
49
+
50
+ // Check if patch is already applied by looking for our marker
51
+ const cjsFile = path.join(targetDir, 'dist', 'cjs', 'src', 'index.cjs');
52
+ if (fs.existsSync(cjsFile)) {
53
+ const content = fs.readFileSync(cjsFile, 'utf8');
54
+ if (content.includes('context.sponsorshipPolicyId')) {
55
+ // Patch already applied
56
+ process.exit(0);
57
+ }
58
+ }
59
+
60
+ try {
61
+ execSync(`npx patch-package --patch-dir "${path.dirname(patchFile)}"`, {
62
+ cwd: path.resolve(targetDir, '..', '..'),
63
+ stdio: 'pipe',
64
+ });
65
+ console.log('@gasfree-kit/evm-4337: Applied wdk-safe-relay-kit patch (Candide paymaster fix)');
66
+ } catch {
67
+ // patch-package not available — try git apply
68
+ try {
69
+ execSync(`git apply --directory="${targetDir}" "${patchFile}"`, {
70
+ stdio: 'pipe',
71
+ });
72
+ console.log('@gasfree-kit/evm-4337: Applied wdk-safe-relay-kit patch via git apply');
73
+ } catch {
74
+ console.warn(
75
+ '@gasfree-kit/evm-4337: Could not auto-apply wdk-safe-relay-kit patch.\n' +
76
+ 'Please apply manually: patches/@tetherto__wdk-safe-relay-kit@4.1.5.patch\n' +
77
+ 'See: https://github.com/gasfree-kit/evm-4337#patch',
78
+ );
79
+ }
80
+ }