@gasfree-kit/evm-4337 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # @gasfree-kit/evm-4337
2
+
3
+ ERC-4337 USDT transfers for EVM chains using WDK-backed Safe smart accounts.
4
+
5
+ This package gives you:
6
+
7
+ - standard seed-phrase driven transfers
8
+ - sponsored mode, where a paymaster covers gas
9
+ - token-paid mode, where gas is charged in USDT
10
+ - optional passkey linking and passkey-signed transfers
11
+
12
+ ## How It Works
13
+
14
+ ```
15
+ ┌──────────────────────┐
16
+ │ Your app │
17
+ └──────────┬───────────┘
18
+
19
+ v
20
+ ┌──────────────────────┐
21
+ │ @gasfree-kit/evm-4337│
22
+ └──────────┬───────────┘
23
+
24
+ v
25
+ ┌──────────────────────┐ ┌──────────────────────┐
26
+ │ WDK wallet manager │──────>│ Safe smart account │
27
+ └──────────────────────┘ └──────────┬───────────┘
28
+
29
+ v
30
+ ┌──────────────────────┐
31
+ ┌──────────────────────┐ │ UserOperation │
32
+ │ Paymaster │·····> │ (ERC-4337) │
33
+ │ │ └──────────┬───────────┘
34
+ │ sponsors gas or │ │
35
+ │ charges in USDT │ v
36
+ └──────────────────────┘ ┌──────────────────────┐
37
+ │ Bundler │
38
+ └──────────┬───────────┘
39
+
40
+ v
41
+ ┌──────────────────────┐
42
+ │ EVM chain │
43
+ └──────────────────────┘
44
+ ```
45
+
46
+ ```mermaid
47
+ flowchart TD
48
+ A["Your app"] --> B["@gasfree-kit/evm-4337"]
49
+ B --> C["WDK wallet manager"]
50
+ C --> D["Safe smart account"]
51
+ D --> E["ERC-4337 UserOperation"]
52
+ F["Paymaster"] -.->|sponsors or charges USDT| E
53
+ E --> G["Bundler"]
54
+ G --> H["EVM chain"]
55
+ ```
56
+
57
+ ## Supported Chains
58
+
59
+ | Chain | Chain ID | Typical fallback fee estimate |
60
+ | -------- | -------- | ----------------------------- |
61
+ | Ethereum | 1 | `1.40` USDT |
62
+ | Base | 8453 | `0.15` USDT |
63
+ | Arbitrum | 42161 | `0.10` USDT |
64
+ | Optimism | 10 | `0.10` USDT |
65
+ | Polygon | 137 | `0.10` USDT |
66
+ | Celo | 42220 | `0.05` USDT |
67
+ | Plasma | 9745 | `0.05` USDT |
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ npm install @gasfree-kit/evm-4337
73
+ ```
74
+
75
+ Required peer dependency:
76
+
77
+ ```bash
78
+ npm install @tetherto/wdk-wallet-evm-erc-4337
79
+ ```
80
+
81
+ Optional passkey peer dependencies:
82
+
83
+ ```bash
84
+ npm install @safe-global/protocol-kit @safe-global/relay-kit
85
+ ```
86
+
87
+ ## Choose Your Gas Mode
88
+
89
+ | Mode | When to use it | Required config |
90
+ | ---------- | --------------------------------- | ------------------------------------------------ |
91
+ | Sponsored | Your paymaster fully covers gas | `isSponsored: true` and `sponsorshipPolicyId` |
92
+ | Token-paid | Gas is deducted from USDT balance | `isSponsored: false` and a paymaster/token setup |
93
+
94
+ ## Quick Start
95
+
96
+ ### 1. Create a config
97
+
98
+ Sponsored mode:
99
+
100
+ ```ts
101
+ import type { EVM4337ClientConfig } from '@gasfree-kit/evm-4337';
102
+
103
+ const sponsoredConfig: EVM4337ClientConfig = {
104
+ chain: 'base',
105
+ rpcUrl: 'https://mainnet.base.org',
106
+ bundlerUrl: 'https://your-bundler.example.com',
107
+ paymasterUrl: 'https://your-paymaster.example.com',
108
+ isSponsored: true,
109
+ sponsorshipPolicyId: 'your-policy-id',
110
+ };
111
+ ```
112
+
113
+ Token-paid mode:
114
+
115
+ ```ts
116
+ const tokenPaidConfig: EVM4337ClientConfig = {
117
+ chain: 'base',
118
+ rpcUrl: 'https://mainnet.base.org',
119
+ bundlerUrl: 'https://your-bundler.example.com',
120
+ paymasterUrl: 'https://your-paymaster.example.com',
121
+ isSponsored: false,
122
+ paymasterAddress: '0xYourPaymasterAddress',
123
+ // Optional:
124
+ // paymasterTokenAddress: '0xYourUSDTLikeToken'
125
+ };
126
+ ```
127
+
128
+ ### 2. Generate a seed phrase
129
+
130
+ ```ts
131
+ import { generateSeedPhrase } from '@gasfree-kit/core';
132
+
133
+ const seedPhrase = await generateSeedPhrase();
134
+ ```
135
+
136
+ ### 3. Set up the Safe smart account
137
+
138
+ ```ts
139
+ import { setupErc4337Wallet } from '@gasfree-kit/evm-4337';
140
+
141
+ const { wallet, account, address } = await setupErc4337Wallet(seedPhrase, sponsoredConfig);
142
+
143
+ console.log(address); // Safe address
144
+ ```
145
+
146
+ ### 4. Check balance
147
+
148
+ ```ts
149
+ import { EvmTransfer } from '@gasfree-kit/evm-4337';
150
+ import { EVM_CHAINS } from '@gasfree-kit/core';
151
+
152
+ const balance = await EvmTransfer.checkTokenBalance(
153
+ seedPhrase,
154
+ sponsoredConfig,
155
+ EVM_CHAINS.base.usdtAddress,
156
+ );
157
+
158
+ console.log(balance.data.usdBalance);
159
+ ```
160
+
161
+ ### 5. Estimate fees
162
+
163
+ ```ts
164
+ const estimate = await EvmTransfer.getTransactionEstimateFee(
165
+ seedPhrase,
166
+ sponsoredConfig,
167
+ '0x1111111111111111111111111111111111111111',
168
+ );
169
+
170
+ console.log(estimate.data.fee);
171
+ ```
172
+
173
+ ### 6. Send a transfer
174
+
175
+ ```ts
176
+ const result = await EvmTransfer.sendToken(
177
+ seedPhrase,
178
+ sponsoredConfig,
179
+ EVM_CHAINS.base.usdtAddress,
180
+ '50.00',
181
+ '0x1111111111111111111111111111111111111111',
182
+ );
183
+
184
+ console.log(result.transactionHash);
185
+ ```
186
+
187
+ ### 7. Send a batch transfer
188
+
189
+ ```ts
190
+ const batch = await EvmTransfer.sendBatchToken(
191
+ seedPhrase,
192
+ sponsoredConfig,
193
+ EVM_CHAINS.base.usdtAddress,
194
+ [
195
+ { address: '0x1111111111111111111111111111111111111111', amount: '25.00' },
196
+ { address: '0x2222222222222222222222222222222222222222', amount: '10.00' },
197
+ ],
198
+ );
199
+ ```
200
+
201
+ ## Passkey Flow
202
+
203
+ Passkeys are optional. They let you add a WebAuthn signer to an existing Safe so transfers can be approved with biometrics instead of a seed phrase.
204
+
205
+ Important:
206
+
207
+ - the Safe must already be deployed on-chain before you link a passkey
208
+ - `storage: 'persist'` is convenience storage only, not hardened secret storage
209
+
210
+ ### Passkey diagram
211
+
212
+ ```
213
+ ┌──────────────────────┐
214
+ │ Seed phrase owner │──────┐
215
+ └──────────────────────┘ │
216
+ v
217
+ ┌──────────────────────┐
218
+ │ Safe smart account │
219
+ └──────────┬───────────┘
220
+ ┌──────────────────────┐ │
221
+ │ Passkey (WebAuthn) │──────┘
222
+ └──────────────────────┘
223
+
224
+ v
225
+ ┌──────────────────────┐
226
+ │ Signed UserOperation │
227
+ └──────────┬───────────┘
228
+
229
+ v
230
+ ┌──────────────────────┐
231
+ │ Bundler + Paymaster │
232
+ └──────────┬───────────┘
233
+
234
+ v
235
+ ┌──────────────────────┐
236
+ │ EVM chain │
237
+ └──────────────────────┘
238
+ ```
239
+
240
+ ```mermaid
241
+ flowchart TD
242
+ A["Seed phrase owner"] --> C["Safe smart account"]
243
+ B["Passkey (WebAuthn)"] --> C
244
+ C --> D["Signed UserOperation"]
245
+ D --> E["Bundler + Paymaster"]
246
+ E --> F["EVM chain"]
247
+ ```
248
+
249
+ ### Link a passkey to an existing Safe
250
+
251
+ ```ts
252
+ import { linkPasskeyToSafe } from '@gasfree-kit/evm-4337';
253
+
254
+ const passkey = await linkPasskeyToSafe(seedPhrase, sponsoredConfig, {
255
+ rpName: 'My App',
256
+ userName: 'user@example.com',
257
+ storage: 'not_persist',
258
+ });
259
+
260
+ console.log(passkey.safeAddress);
261
+ console.log(passkey.credential.signerAddress);
262
+ ```
263
+
264
+ ### Check whether a passkey is linked
265
+
266
+ ```ts
267
+ import { isPasskeyLinked } from '@gasfree-kit/evm-4337';
268
+
269
+ const status = await isPasskeyLinked(seedPhrase, sponsoredConfig);
270
+
271
+ if (status.linked) {
272
+ console.log(status.signerAddress);
273
+ }
274
+ ```
275
+
276
+ ### Send a transfer with a passkey
277
+
278
+ ```ts
279
+ import { PasskeyTransfer } from '@gasfree-kit/evm-4337';
280
+ import { EVM_CHAINS } from '@gasfree-kit/core';
281
+
282
+ const tx = await PasskeyTransfer.sendToken(
283
+ passkey.credential,
284
+ sponsoredConfig,
285
+ EVM_CHAINS.base.usdtAddress,
286
+ '10.00',
287
+ '0x1111111111111111111111111111111111111111',
288
+ );
289
+ ```
290
+
291
+ ### Remove a passkey owner
292
+
293
+ ```ts
294
+ import { unlinkPasskeyFromSafe } from '@gasfree-kit/evm-4337';
295
+
296
+ await unlinkPasskeyFromSafe(seedPhrase, sponsoredConfig, passkey.credential);
297
+ ```
298
+
299
+ ## Main Exports
300
+
301
+ | Export | What it does |
302
+ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
303
+ | `setupErc4337Wallet` | Creates a WDK-backed ERC-4337 wallet and resolves the Safe address |
304
+ | `EvmTransfer` | Estimates, checks balance, sends single transfers, and sends batch transfers |
305
+ | `PasskeyTransfer` | Sends passkey-signed transfers after a passkey has been linked |
306
+ | `linkPasskeyToSafe` | Registers a passkey and adds its signer to the Safe owner set |
307
+ | `isPasskeyLinked` | Checks whether a stored passkey is still an owner on-chain |
308
+ | `unlinkPasskeyFromSafe` | Removes a passkey signer from the Safe |
309
+ | `getErc4337ConfigForChain` | Normalizes the public client config into runtime config |
310
+ | `GAS_FEE_FALLBACKS` and `GAS_FEE_ESTIMATES` | Exposes fallback fee heuristics per chain |
311
+ | `toUsdtBaseUnitsEvm`, `formatTokenBalance`, `feeToUsdt`, `encodeErc20Transfer` | Utility helpers for amounts and calldata |
312
+
313
+ ## Export Example
314
+
315
+ ```ts
316
+ import {
317
+ setupErc4337Wallet,
318
+ EvmTransfer,
319
+ PasskeyTransfer,
320
+ linkPasskeyToSafe,
321
+ isPasskeyLinked,
322
+ unlinkPasskeyFromSafe,
323
+ getErc4337ConfigForChain,
324
+ GAS_FEE_FALLBACKS,
325
+ GAS_FEE_ESTIMATES,
326
+ } from '@gasfree-kit/evm-4337';
327
+ ```
328
+
329
+ ## Notes
330
+
331
+ - `sponsorshipPolicyId` is required when `isSponsored` is `true`
332
+ - self-transfers are blocked
333
+ - token-paid mode checks that the USDT balance can cover the transfer and gas reserve
334
+ - passkey linking verifies the signer deployment before adding it as an owner
335
+
336
+ ## License
337
+
338
+ MIT
package/dist/index.d.mts CHANGED
@@ -40,6 +40,54 @@ declare const GAS_FEE_FALLBACKS: Record<string, bigint>;
40
40
  /** Fallback fee estimates as human-readable strings. */
41
41
  declare const GAS_FEE_ESTIMATES: Record<string, string>;
42
42
 
43
+ /** Passkey credential data — app developer must persist this (or use 'persist' storage mode). */
44
+ interface PasskeyCredential {
45
+ /** WebAuthn credential ID (base64url encoded) */
46
+ id: string;
47
+ /** P256 public key coordinates from WebAuthn attestation */
48
+ publicKey: {
49
+ x: bigint;
50
+ y: bigint;
51
+ };
52
+ /** On-chain SafeWebAuthnSigner contract address */
53
+ signerAddress: string;
54
+ /** The Safe smart account this passkey is linked to */
55
+ safeAddress: string;
56
+ }
57
+ /** Storage mode for passkey credentials. */
58
+ type PasskeyStorageMode = 'persist' | 'not_persist';
59
+ /** Result from linking a passkey to a Safe. */
60
+ interface PasskeyLinkResult {
61
+ /** Passkey credential — always returned regardless of storage mode */
62
+ credential: PasskeyCredential;
63
+ /** Safe smart account address */
64
+ safeAddress: string;
65
+ /** Transaction hash of the addOwner operation */
66
+ transactionHash: string;
67
+ /** Whether the SDK persisted the credential internally */
68
+ persisted: boolean;
69
+ }
70
+ /** Options for passkey registration and linking. */
71
+ interface PasskeyOptions {
72
+ /** Relying party ID (defaults to window.location.hostname) */
73
+ rpId?: string;
74
+ /** Relying party display name */
75
+ rpName?: string;
76
+ /** User display name for the passkey credential */
77
+ userName?: string;
78
+ /** Credential storage mode. Default: 'not_persist'. Convenience only; not secure storage. */
79
+ storage?: PasskeyStorageMode;
80
+ }
81
+ /** Config extending EVM4337ClientConfig with passkey-specific options. */
82
+ interface PasskeyConfig extends EVM4337ClientConfig {
83
+ /** WebAuthn and passkey registration options */
84
+ passkeyOptions?: PasskeyOptions;
85
+ }
86
+ /** Pre-deployed SafeWebAuthnSignerFactory addresses per chain. */
87
+ declare const SAFE_WEBAUTHN_SIGNER_FACTORY: Record<string, string>;
88
+ /** Pre-deployed FCLP256Verifier addresses per chain. */
89
+ declare const FCL_P256_VERIFIER: Record<string, string>;
90
+
43
91
  /**
44
92
  * Set up an ERC-4337 wallet via WDK.
45
93
  *
@@ -47,21 +95,17 @@ declare const GAS_FEE_ESTIMATES: Record<string, string>;
47
95
  * correct discriminated-union config:
48
96
  * - Sponsored mode → EvmErc4337WalletSponsorshipPolicyConfig
49
97
  * - Non-sponsored → EvmErc4337WalletPaymasterTokenConfig
98
+ *
99
+ * Optionally links a passkey to the Safe account when passkeyOptions is provided.
50
100
  */
51
- declare function setupErc4337Wallet(seedPhrase: string, config: EVM4337ClientConfig, accountIndex?: number): Promise<{
101
+ declare function setupErc4337Wallet(seedPhrase: string, config: EVM4337ClientConfig, accountIndex?: number, passkeyOptions?: PasskeyOptions): Promise<{
52
102
  wallet: _tetherto_wdk_wallet_evm_erc_4337.default;
53
103
  account: _tetherto_wdk_wallet_evm_erc_4337.EvmAccount;
54
104
  address: string;
105
+ passkey: PasskeyLinkResult | undefined;
55
106
  }>;
56
107
 
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;
108
+ declare class EvmTransfer {
65
109
  /**
66
110
  * Get transaction fee estimate for a USDT transfer.
67
111
  *
@@ -117,10 +161,6 @@ declare class TetherEVMERC4337Transfer {
117
161
  amount: string;
118
162
  }[];
119
163
  }>;
120
- /**
121
- * Map raw errors into user-friendly messages.
122
- */
123
- static handleTransferError(error: unknown, chain: string): Error;
124
164
  }
125
165
 
126
166
  /**
@@ -143,4 +183,111 @@ declare function encodeErc20Transfer(to: string, amount: bigint): string;
143
183
  */
144
184
  declare function feeToUsdt(feeInWei: bigint, nativeTokenPriceUsdt: number): string;
145
185
 
146
- export { type EVM4337ClientConfig, type EvmErc4337NetworkConfig, GAS_FEE_ESTIMATES, GAS_FEE_FALLBACKS, TetherEVMERC4337Transfer, encodeErc20Transfer, feeToUsdt, formatTokenBalance, getErc4337ConfigForChain, setupErc4337Wallet, toUsdtBaseUnitsEvm };
186
+ /**
187
+ * Wait for a UserOperation to be included on-chain via the Candide bundler.
188
+ * Times out after USER_OP_TIMEOUT_MS to prevent indefinite hangs.
189
+ */
190
+ declare function waitForUserOpConfirmation(userOpHash: string, bundlerUrl: string, entryPointAddress: string): Promise<string>;
191
+ /**
192
+ * Map raw errors into user-friendly messages.
193
+ */
194
+ declare function handleTransferError(error: unknown, chain: string): Error;
195
+
196
+ /**
197
+ * Register a new passkey via WebAuthn and link it to an existing Safe account.
198
+ *
199
+ * 1. Validates the Safe is deployed on-chain
200
+ * 2. Registers a new WebAuthn passkey (triggers biometric prompt)
201
+ * 3. Checks the signer is not already a Safe owner
202
+ * 4. Deploys the SafeWebAuthnSigner contract on-chain via the factory
203
+ * 5. Adds the signer as a Safe owner with threshold 1 (1-of-2)
204
+ *
205
+ * The seed phrase signs both the deploy and addOwner transactions.
206
+ */
207
+ declare function linkPasskeyToSafe(seedPhrase: string, config: EVM4337ClientConfig, passkeyOptions?: PasskeyOptions, accountIndex?: number): Promise<PasskeyLinkResult>;
208
+ /**
209
+ * Check if a Safe account already has a passkey signer linked.
210
+ *
211
+ * Fix #5: properly disposes wallet/account resources.
212
+ */
213
+ declare function isPasskeyLinked(seedPhrase: string, config: EVM4337ClientConfig, accountIndex?: number): Promise<{
214
+ linked: boolean;
215
+ signerAddress?: string;
216
+ }>;
217
+ /**
218
+ * Remove a passkey signer from the Safe.
219
+ * Signs the removeOwner tx with the seed phrase.
220
+ *
221
+ * Fix #1: Queries the actual Safe owner list to determine the correct
222
+ * previous owner in the linked list, instead of hardcoding SENTINEL.
223
+ */
224
+ declare function unlinkPasskeyFromSafe(seedPhrase: string, config: EVM4337ClientConfig, credential: PasskeyCredential, accountIndex?: number): Promise<{
225
+ transactionHash: string;
226
+ }>;
227
+
228
+ /**
229
+ * Passkey-signed ERC-4337 transfers via Safe account.
230
+ *
231
+ * Uses Safe4337Pack from @safe-global/relay-kit to handle the full
232
+ * UserOperation lifecycle: construction, passkey signing (WebAuthn biometric),
233
+ * paymaster integration, and bundler submission.
234
+ *
235
+ * The passkey must first be linked to the Safe via `linkPasskeyToSafe()`.
236
+ */
237
+ declare class PasskeyTransfer {
238
+ /**
239
+ * Send a single token transfer signed by passkey.
240
+ * Triggers biometric authentication via WebAuthn.
241
+ */
242
+ static sendToken(credential: PasskeyCredential, config: EVM4337ClientConfig, tokenAddress: string, amount: string, recipientAddress: string): Promise<{
243
+ success: boolean;
244
+ transactionHash: string;
245
+ chain: _gasfree_kit_core.EvmChain;
246
+ amount: string;
247
+ }>;
248
+ /**
249
+ * Send a batch token transfer to multiple recipients, signed by passkey.
250
+ */
251
+ static sendBatchToken(credential: PasskeyCredential, config: EVM4337ClientConfig, tokenAddress: string, recipients: Array<{
252
+ address: string;
253
+ amount: string;
254
+ }>): Promise<{
255
+ success: boolean;
256
+ transactionHash: string;
257
+ chain: _gasfree_kit_core.EvmChain;
258
+ recipients: {
259
+ address: string;
260
+ amount: string;
261
+ }[];
262
+ }>;
263
+ /**
264
+ * Check token balance for a passkey-linked Safe account.
265
+ * Does NOT require biometric — balance is publicly readable via RPC.
266
+ */
267
+ static checkTokenBalance(credential: PasskeyCredential, config: EVM4337ClientConfig, tokenAddress: string): Promise<{
268
+ message: string;
269
+ success: boolean;
270
+ data: {
271
+ tokenBalance: string;
272
+ decimals: number;
273
+ usdBalance: string;
274
+ };
275
+ }>;
276
+ }
277
+
278
+ /** Interface for passkey credential persistence. */
279
+ interface CredentialStore {
280
+ save(credential: PasskeyCredential): Promise<void>;
281
+ load(safeAddress: string): Promise<PasskeyCredential | null>;
282
+ remove(safeAddress: string): Promise<void>;
283
+ listAll(): Promise<PasskeyCredential[]>;
284
+ }
285
+ /**
286
+ * Auto-detect environment and return the appropriate credential store.
287
+ *
288
+ * Fix #10: Stricter browser detection — checks for navigator.credentials
289
+ * to avoid false positives in SSR/jsdom environments.
290
+ */
291
+ declare function createCredentialStore(): CredentialStore;
292
+
293
+ export { type CredentialStore, type EVM4337ClientConfig, type EvmErc4337NetworkConfig, EvmTransfer, FCL_P256_VERIFIER, GAS_FEE_ESTIMATES, GAS_FEE_FALLBACKS, type PasskeyConfig, type PasskeyCredential, type PasskeyLinkResult, type PasskeyOptions, type PasskeyStorageMode, PasskeyTransfer, SAFE_WEBAUTHN_SIGNER_FACTORY, createCredentialStore, encodeErc20Transfer, feeToUsdt, formatTokenBalance, getErc4337ConfigForChain, handleTransferError, isPasskeyLinked, linkPasskeyToSafe, setupErc4337Wallet, toUsdtBaseUnitsEvm, unlinkPasskeyFromSafe, waitForUserOpConfirmation };