@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.
- package/dist/index.d.mts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +459 -0
- package/dist/index.mjs +418 -0
- package/package.json +39 -0
- package/patches/@tetherto__wdk-safe-relay-kit@4.1.5.patch +68 -0
- package/scripts/postinstall.js +80 -0
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|