erc4337-kit 0.1.0 → 0.1.1
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 +368 -70
- package/package.json +17 -23
package/README.md
CHANGED
|
@@ -1,167 +1,465 @@
|
|
|
1
1
|
# erc4337-kit
|
|
2
2
|
|
|
3
|
-
ERC-4337 Account Abstraction for React
|
|
3
|
+
> ERC-4337 Account Abstraction for React — gasless transactions, social login, and smart accounts without the complexity.
|
|
4
4
|
|
|
5
|
-
Built on
|
|
5
|
+
Built on **Privy** (auth) · **Pimlico** (bundler + paymaster) · **Permissionless** (smart accounts) · **Polygon Amoy** (default chain)
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/erc4337-kit)
|
|
8
|
+
[](LICENSE)
|
|
6
9
|
|
|
7
10
|
---
|
|
8
11
|
|
|
9
|
-
##
|
|
12
|
+
## What this package does
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
Normally, setting up ERC-4337 means wiring together Privy, Permissionless, Pimlico, viem, wagmi, and writing ~200 lines of boilerplate hooks yourself — dealing with race conditions, polyfills, gas estimation, UserOperation formatting, and paymaster sponsorship.
|
|
15
|
+
|
|
16
|
+
This package collapses all of that into **three exports**: a provider, a hook, and a transaction hook.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Without erc4337-kit: With erc4337-kit:
|
|
20
|
+
───────────────────── ─────────────────────
|
|
21
|
+
200 lines of setup → <ChainProvider> (5 lines)
|
|
22
|
+
Privy + wagmi + QueryClient useSmartAccount() (1 line)
|
|
23
|
+
Smart account init race fix useStoreOnChain() (1 line)
|
|
24
|
+
Pimlico gas estimation
|
|
25
|
+
UserOperation formatting
|
|
26
|
+
Error parsing
|
|
13
27
|
```
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- React 18 or 19
|
|
34
|
+
- Vite (Next.js support coming)
|
|
35
|
+
- Node.js 18+
|
|
36
|
+
- A Privy App ID (free at [dashboard.privy.io](https://dashboard.privy.io))
|
|
37
|
+
- A Pimlico API Key (free at [dashboard.pimlico.io](https://dashboard.pimlico.io))
|
|
38
|
+
- An Alchemy RPC URL (free at [dashboard.alchemy.com](https://dashboard.alchemy.com))
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
16
43
|
|
|
17
44
|
```bash
|
|
18
|
-
|
|
45
|
+
# Step 1: install the package
|
|
46
|
+
npm install erc4337-kit
|
|
47
|
+
|
|
48
|
+
# Step 2: install peer dependencies
|
|
49
|
+
npm install @privy-io/react-auth @privy-io/wagmi viem wagmi @tanstack/react-query
|
|
50
|
+
|
|
51
|
+
# Step 3: install browser polyfills (viem needs these)
|
|
52
|
+
npm install buffer process
|
|
19
53
|
```
|
|
20
54
|
|
|
21
55
|
---
|
|
22
56
|
|
|
23
|
-
##
|
|
57
|
+
## Setup (two files to edit, then you're done)
|
|
24
58
|
|
|
25
|
-
|
|
59
|
+
### 1. `vite.config.js`
|
|
26
60
|
|
|
27
61
|
```js
|
|
62
|
+
import { defineConfig } from 'vite'
|
|
63
|
+
import react from '@vitejs/plugin-react'
|
|
64
|
+
|
|
28
65
|
export default defineConfig({
|
|
29
|
-
|
|
66
|
+
plugins: [react()],
|
|
67
|
+
define: {
|
|
68
|
+
global: 'globalThis', // required for viem
|
|
69
|
+
},
|
|
30
70
|
resolve: {
|
|
31
|
-
alias: {
|
|
71
|
+
alias: {
|
|
72
|
+
'@noble/curves/nist.js': '@noble/curves/nist', // required for permissionless
|
|
73
|
+
},
|
|
32
74
|
},
|
|
33
75
|
})
|
|
34
76
|
```
|
|
35
77
|
|
|
36
|
-
|
|
78
|
+
> If you're using Tailwind v4, add `tailwindcss from '@tailwindcss/vite'` to plugins as normal — it's compatible.
|
|
79
|
+
|
|
80
|
+
### 2. `index.html` — add this in `<head>`, **before** your app script
|
|
37
81
|
|
|
38
82
|
```html
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
83
|
+
<head>
|
|
84
|
+
<!-- ... your other meta tags ... -->
|
|
85
|
+
|
|
86
|
+
<!-- REQUIRED: add this before <script src="/src/main.jsx"> -->
|
|
87
|
+
<script type="module">
|
|
88
|
+
import { Buffer } from 'buffer'
|
|
89
|
+
import process from 'process'
|
|
90
|
+
window.Buffer = Buffer
|
|
91
|
+
window.process = process
|
|
92
|
+
</script>
|
|
93
|
+
</head>
|
|
45
94
|
```
|
|
46
95
|
|
|
96
|
+
> **Why?** `viem` and `permissionless` use Node.js globals (`Buffer`, `process`) that don't exist in the browser. This polyfill must load before your app or you'll get `ReferenceError: Buffer is not defined`.
|
|
97
|
+
|
|
47
98
|
---
|
|
48
99
|
|
|
49
|
-
##
|
|
100
|
+
## `.env`
|
|
50
101
|
|
|
51
|
-
|
|
102
|
+
```env
|
|
103
|
+
VITE_PRIVY_APP_ID= # from dashboard.privy.io → your app → App ID
|
|
104
|
+
VITE_PIMLICO_API_KEY= # from dashboard.pimlico.io → API Keys
|
|
105
|
+
VITE_RPC_URL= # from dashboard.alchemy.com → Polygon Amoy → HTTPS URL
|
|
106
|
+
VITE_CONTRACT_ADDRESS= # your deployed contract address (after you deploy)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> Never commit `.env` to git. Add it to `.gitignore`.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Usage
|
|
114
|
+
|
|
115
|
+
### Step 1 — Wrap your app with `ChainProvider`
|
|
116
|
+
|
|
117
|
+
Put this in `src/main.jsx`. It sets up Privy, QueryClient, and Wagmi in one shot.
|
|
52
118
|
|
|
53
119
|
```jsx
|
|
54
|
-
import
|
|
55
|
-
import
|
|
120
|
+
import React from 'react'
|
|
121
|
+
import ReactDOM from 'react-dom/client'
|
|
122
|
+
import { ChainProvider, polygonAmoy } from 'erc4337-kit'
|
|
123
|
+
import App from './App.jsx'
|
|
56
124
|
|
|
57
|
-
|
|
58
|
-
|
|
125
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
126
|
+
<React.StrictMode>
|
|
59
127
|
<ChainProvider
|
|
60
128
|
privyAppId={import.meta.env.VITE_PRIVY_APP_ID}
|
|
61
129
|
chain={polygonAmoy}
|
|
62
130
|
rpcUrl={import.meta.env.VITE_RPC_URL}
|
|
131
|
+
loginMethods={['google', 'email']} // optional, this is the default
|
|
132
|
+
appearance={{ theme: 'dark', accentColor: '#7c3aed' }} // optional
|
|
63
133
|
>
|
|
64
134
|
<App />
|
|
65
135
|
</ChainProvider>
|
|
66
|
-
|
|
67
|
-
|
|
136
|
+
</React.StrictMode>,
|
|
137
|
+
)
|
|
68
138
|
```
|
|
69
139
|
|
|
70
|
-
### 2
|
|
140
|
+
### Step 2 — Initialize the smart account
|
|
71
141
|
|
|
72
142
|
```jsx
|
|
73
|
-
import { useSmartAccount } from 'erc4337-kit'
|
|
74
|
-
import { polygonAmoy } from 'erc4337-kit'
|
|
143
|
+
import { useSmartAccount, polygonAmoy } from 'erc4337-kit'
|
|
75
144
|
|
|
76
145
|
function App() {
|
|
77
146
|
const {
|
|
78
|
-
login,
|
|
79
|
-
|
|
80
|
-
|
|
147
|
+
login, // Function — opens Privy login modal
|
|
148
|
+
logout, // Function — clears all state
|
|
149
|
+
authenticated, // boolean — user is logged in
|
|
150
|
+
user, // Privy user object (has .email.address, .google.email)
|
|
151
|
+
smartAccountAddress, // string — the user's smart account address (0x...)
|
|
152
|
+
smartAccountClient, // SmartAccountClient — use this to send transactions
|
|
153
|
+
isReady, // boolean — smart account is initialized, safe to transact
|
|
154
|
+
isLoading, // boolean — still setting up
|
|
155
|
+
error, // string | null — human-readable error message
|
|
81
156
|
} = useSmartAccount({
|
|
82
157
|
pimlicoApiKey: import.meta.env.VITE_PIMLICO_API_KEY,
|
|
83
158
|
rpcUrl: import.meta.env.VITE_RPC_URL,
|
|
84
159
|
chain: polygonAmoy,
|
|
85
160
|
})
|
|
86
161
|
|
|
87
|
-
if (!authenticated) return <button onClick={login}>
|
|
88
|
-
if (isLoading)
|
|
89
|
-
if (error)
|
|
162
|
+
if (!authenticated) return <button onClick={login}>Sign in</button>
|
|
163
|
+
if (isLoading) return <p>Setting up your wallet…</p>
|
|
164
|
+
if (error) return <p style={{ color: 'red' }}>Error: {error}</p>
|
|
90
165
|
|
|
91
|
-
return
|
|
166
|
+
return (
|
|
167
|
+
<div>
|
|
168
|
+
<p>Smart account: {smartAccountAddress}</p>
|
|
169
|
+
<button onClick={logout}>Sign out</button>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
92
172
|
}
|
|
93
173
|
```
|
|
94
174
|
|
|
95
|
-
|
|
175
|
+
> `smartAccountAddress` is **deterministic** — the same user always gets the same address across sessions. It is a smart contract address, not the user's EOA (embedded wallet). Store this in your database, not the Privy user ID, if you need to link on-chain records to users.
|
|
176
|
+
|
|
177
|
+
### Step 3 — Send a gasless transaction
|
|
178
|
+
|
|
179
|
+
#### Option A: use `useStoreOnChain` (simplest — for hash-based data storage)
|
|
96
180
|
|
|
97
181
|
```jsx
|
|
98
182
|
import { useStoreOnChain, sha256Hash } from 'erc4337-kit'
|
|
99
183
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
},
|
|
106
|
-
]
|
|
184
|
+
const MY_ABI = [{
|
|
185
|
+
name: 'storeRecord',
|
|
186
|
+
type: 'function',
|
|
187
|
+
inputs: [{ name: 'dataHash', type: 'bytes32' }],
|
|
188
|
+
}]
|
|
107
189
|
|
|
108
|
-
function
|
|
109
|
-
const {
|
|
190
|
+
function SubmitForm({ smartAccountClient }) {
|
|
191
|
+
const {
|
|
192
|
+
submit, // async (args: any[]) => string | null — returns txHash
|
|
193
|
+
txHash, // string | null
|
|
194
|
+
recordId, // string | null — decoded bytes32 from first event log
|
|
195
|
+
isLoading, // boolean
|
|
196
|
+
isSuccess, // boolean
|
|
197
|
+
error, // string | null
|
|
198
|
+
reset, // Function — resets all state back to null
|
|
199
|
+
} = useStoreOnChain({
|
|
110
200
|
smartAccountClient,
|
|
111
201
|
contractAddress: import.meta.env.VITE_CONTRACT_ADDRESS,
|
|
112
|
-
abi:
|
|
202
|
+
abi: MY_ABI,
|
|
113
203
|
functionName: 'storeRecord',
|
|
114
204
|
})
|
|
115
205
|
|
|
116
|
-
const handleSubmit = async (
|
|
117
|
-
const hash = await sha256Hash(
|
|
118
|
-
await submit([hash])
|
|
206
|
+
const handleSubmit = async (rawData) => {
|
|
207
|
+
const hash = await sha256Hash(JSON.stringify(rawData)) // hash locally
|
|
208
|
+
await submit([hash]) // send on-chain
|
|
119
209
|
}
|
|
120
210
|
|
|
121
211
|
return (
|
|
122
212
|
<div>
|
|
123
|
-
<button onClick={() => handleSubmit('
|
|
124
|
-
{isLoading ? 'Storing
|
|
213
|
+
<button onClick={() => handleSubmit({ text: 'my data' })} disabled={isLoading}>
|
|
214
|
+
{isLoading ? 'Storing…' : 'Submit'}
|
|
125
215
|
</button>
|
|
126
|
-
{
|
|
127
|
-
{error && <p
|
|
216
|
+
{isSuccess && <p>Stored! Tx: <a href={`https://amoy.polygonscan.com/tx/${txHash}`}>{txHash?.slice(0,10)}…</a></p>}
|
|
217
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
128
218
|
</div>
|
|
129
219
|
)
|
|
130
220
|
}
|
|
131
221
|
```
|
|
132
222
|
|
|
223
|
+
#### Option B: use `smartAccountClient.sendTransaction` directly (for any contract call)
|
|
224
|
+
|
|
225
|
+
```jsx
|
|
226
|
+
import { encodeFunctionData } from 'viem'
|
|
227
|
+
|
|
228
|
+
const handleAddTodo = async (task) => {
|
|
229
|
+
const calldata = encodeFunctionData({
|
|
230
|
+
abi: contractABI,
|
|
231
|
+
functionName: 'addTodo',
|
|
232
|
+
args: [task],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// ✅ Correct — use sendTransaction with encoded calldata
|
|
236
|
+
const hash = await smartAccountClient.sendTransaction({
|
|
237
|
+
to: contractAddress,
|
|
238
|
+
data: calldata,
|
|
239
|
+
value: 0n, // no ETH/MATIC being sent
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
console.log('tx hash:', hash)
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
> **Critical:** Do NOT use `smartAccountClient.writeContract()`. The smart account client uses `sendTransaction` with `encodeFunctionData`. Calling `writeContract` throws `account.encodeCalls is not a function`.
|
|
247
|
+
|
|
248
|
+
### Step 4 — Read from the contract
|
|
249
|
+
|
|
250
|
+
For reading, create a standard `publicClient` from viem. Reading is free (no gas, no smart account needed).
|
|
251
|
+
|
|
252
|
+
```jsx
|
|
253
|
+
import { createPublicClient, http } from 'viem'
|
|
254
|
+
import { polygonAmoy } from 'erc4337-kit'
|
|
255
|
+
|
|
256
|
+
const publicClient = createPublicClient({
|
|
257
|
+
chain: polygonAmoy,
|
|
258
|
+
transport: http(import.meta.env.VITE_RPC_URL),
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// For user-specific data, pass account: smartAccountAddress
|
|
262
|
+
const todos = await publicClient.readContract({
|
|
263
|
+
address: contractAddress,
|
|
264
|
+
abi: contractABI,
|
|
265
|
+
functionName: 'getTodos',
|
|
266
|
+
args: [],
|
|
267
|
+
account: smartAccountAddress, // required for mapping(address => ...) returns
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
> `account: smartAccountAddress` is required when your contract uses `msg.sender` to look up data. Without it, the read returns data for address `0x000...000` instead.
|
|
272
|
+
|
|
133
273
|
---
|
|
134
274
|
|
|
135
|
-
##
|
|
275
|
+
## Supported chains
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
import { polygonAmoy, polygon, sepolia, baseSepolia } from 'erc4337-kit'
|
|
279
|
+
```
|
|
136
280
|
|
|
137
|
-
|
|
281
|
+
| Export | Network | Use for |
|
|
282
|
+
|--------|---------|---------|
|
|
283
|
+
| `polygonAmoy` | Polygon Amoy testnet (chain ID 80002) | Development and testing |
|
|
284
|
+
| `polygon` | Polygon mainnet | Production |
|
|
285
|
+
| `sepolia` | Ethereum Sepolia testnet | Ethereum testing |
|
|
286
|
+
| `baseSepolia` | Base Sepolia testnet | Base chain testing |
|
|
138
287
|
|
|
139
|
-
|
|
288
|
+
Any chain supported by both Pimlico and Privy works — these are just the re-exported convenience constants.
|
|
140
289
|
|
|
141
290
|
---
|
|
142
291
|
|
|
143
|
-
##
|
|
292
|
+
## Solidity contract compatibility
|
|
144
293
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
294
|
+
Your contract works with this package without modification. There is one rule you must understand:
|
|
295
|
+
|
|
296
|
+
**`msg.sender` in your contract will be the user's Smart Account address, not their EOA.**
|
|
297
|
+
|
|
298
|
+
This is correct and expected. Your mappings, ownership checks, and identity logic should use `msg.sender` as normal — it will consistently resolve to the user's smart account address every session.
|
|
299
|
+
|
|
300
|
+
A minimal compatible contract:
|
|
301
|
+
|
|
302
|
+
```solidity
|
|
303
|
+
// SPDX-License-Identifier: MIT
|
|
304
|
+
pragma solidity ^0.8.20;
|
|
305
|
+
|
|
306
|
+
contract YourApp {
|
|
307
|
+
// msg.sender = user's Smart Account (consistent, deterministic)
|
|
308
|
+
mapping(address => bytes32[]) private _records;
|
|
309
|
+
|
|
310
|
+
function storeRecord(bytes32 dataHash) external {
|
|
311
|
+
_records[msg.sender].push(dataHash);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getRecords() external view returns (bytes32[] memory) {
|
|
315
|
+
return _records[msg.sender];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
150
318
|
```
|
|
151
319
|
|
|
320
|
+
A template with more complete patterns is included at `node_modules/erc4337-kit/src/contracts/BaseStorage.sol`.
|
|
321
|
+
|
|
152
322
|
---
|
|
153
323
|
|
|
154
|
-
##
|
|
324
|
+
## API reference
|
|
325
|
+
|
|
326
|
+
### `<ChainProvider>`
|
|
327
|
+
|
|
328
|
+
| Prop | Type | Required | Default | Description |
|
|
329
|
+
|------|------|----------|---------|-------------|
|
|
330
|
+
| `privyAppId` | `string` | Yes | — | Your Privy App ID |
|
|
331
|
+
| `chain` | `Chain` (viem) | Yes | — | Target blockchain |
|
|
332
|
+
| `rpcUrl` | `string` | Yes | — | Alchemy / Infura RPC URL |
|
|
333
|
+
| `loginMethods` | `string[]` | No | `['google', 'email']` | Privy login methods |
|
|
334
|
+
| `appearance` | `object` | No | `{ theme: 'light' }` | Privy modal appearance |
|
|
335
|
+
|
|
336
|
+
### `useSmartAccount(config)`
|
|
337
|
+
|
|
338
|
+
**Config:**
|
|
339
|
+
|
|
340
|
+
| Field | Type | Required | Description |
|
|
341
|
+
|-------|------|----------|-------------|
|
|
342
|
+
| `pimlicoApiKey` | `string` | Yes | Pimlico API key |
|
|
343
|
+
| `rpcUrl` | `string` | Yes | RPC URL matching your chain |
|
|
344
|
+
| `chain` | `Chain` (viem) | Yes | Must match ChainProvider |
|
|
345
|
+
|
|
346
|
+
**Returns:**
|
|
347
|
+
|
|
348
|
+
| Field | Type | Description |
|
|
349
|
+
|-------|------|-------------|
|
|
350
|
+
| `login` | `Function` | Opens Privy login modal |
|
|
351
|
+
| `logout` | `Function` | Clears all state and logs out |
|
|
352
|
+
| `authenticated` | `boolean` | True when user is logged in |
|
|
353
|
+
| `user` | `PrivyUser \| null` | Privy user object |
|
|
354
|
+
| `smartAccountAddress` | `string \| null` | The user's smart account address |
|
|
355
|
+
| `smartAccountClient` | `SmartAccountClient \| null` | For sending transactions |
|
|
356
|
+
| `pimlicoClient` | `PimlicoClient \| null` | For gas price reads |
|
|
357
|
+
| `isReady` | `boolean` | True when safe to call `sendTransaction` |
|
|
358
|
+
| `isLoading` | `boolean` | True during initialization |
|
|
359
|
+
| `error` | `string \| null` | Human-readable error |
|
|
360
|
+
|
|
361
|
+
### `useStoreOnChain(config)`
|
|
362
|
+
|
|
363
|
+
**Config:**
|
|
364
|
+
|
|
365
|
+
| Field | Type | Required | Description |
|
|
366
|
+
|-------|------|----------|-------------|
|
|
367
|
+
| `smartAccountClient` | `SmartAccountClient` | Yes | From `useSmartAccount()` |
|
|
368
|
+
| `contractAddress` | `string` | Yes | Deployed contract address |
|
|
369
|
+
| `abi` | `Abi` | Yes | Contract ABI (just the functions you need) |
|
|
370
|
+
| `functionName` | `string` | Yes | Function to call |
|
|
371
|
+
|
|
372
|
+
**Returns:**
|
|
373
|
+
|
|
374
|
+
| Field | Type | Description |
|
|
375
|
+
|-------|------|-------------|
|
|
376
|
+
| `submit` | `async (args: any[]) => string \| null` | Sends the transaction, returns txHash |
|
|
377
|
+
| `txHash` | `string \| null` | Transaction hash after success |
|
|
378
|
+
| `recordId` | `string \| null` | bytes32 decoded from first event log |
|
|
379
|
+
| `isLoading` | `boolean` | True while submitting |
|
|
380
|
+
| `isSuccess` | `boolean` | True after successful submission |
|
|
381
|
+
| `error` | `string \| null` | Human-readable error |
|
|
382
|
+
| `reset` | `Function` | Clears all state back to null |
|
|
383
|
+
|
|
384
|
+
### `sha256Hash(data)` / `sha256HashFile(file)`
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
const hash = await sha256Hash('any string') // → '0x7f3a...' (66 chars)
|
|
388
|
+
const hash = await sha256HashFile(fileObject) // → '0xabcd...' (66 chars)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Both return a `0x`-prefixed hex string that is `bytes32`-compatible. Hashing happens in the browser using the Web Crypto API — no data leaves the device.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Troubleshooting
|
|
396
|
+
|
|
397
|
+
### `ReferenceError: Buffer is not defined`
|
|
398
|
+
The polyfill script is missing from `index.html`, or it's placed after your app script. It must come first in `<head>`.
|
|
399
|
+
|
|
400
|
+
### Smart account not initializing
|
|
401
|
+
Check all three env vars are set and correct. Add `console.log(error)` from `useSmartAccount` to see the exact message. Most commonly: wrong Pimlico API key, or Polygon Amoy not enabled in your Pimlico dashboard.
|
|
402
|
+
|
|
403
|
+
### `account.encodeCalls is not a function`
|
|
404
|
+
You called `smartAccountClient.writeContract()`. Use `smartAccountClient.sendTransaction()` with `encodeFunctionData()` from viem instead. See Option B in usage above.
|
|
405
|
+
|
|
406
|
+
### Contract reads returning empty or wrong data
|
|
407
|
+
You're missing `account: smartAccountAddress` in `publicClient.readContract()`. Without it, reads go out as address `0x0` which returns empty mappings.
|
|
408
|
+
|
|
409
|
+
### `AA21` — paymaster rejected
|
|
410
|
+
Your Pimlico API key is wrong, or Polygon Amoy isn't enabled in your Pimlico project dashboard. The erc4337-kit error message will say this in plain English.
|
|
411
|
+
|
|
412
|
+
### `AA31` — paymaster out of funds
|
|
413
|
+
Your Pimlico paymaster balance is empty. The free tier works for testnet — log in and check your dashboard balance.
|
|
414
|
+
|
|
415
|
+
### `nonce` error
|
|
416
|
+
A previous UserOperation from this smart account is still pending in the bundler mempool. Wait 30–60 seconds and retry.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Production checklist
|
|
421
|
+
|
|
422
|
+
- [ ] Move from Polygon Amoy to Polygon mainnet (change `chain` and `rpcUrl`)
|
|
423
|
+
- [ ] Upgrade Pimlico to a paid plan (free tier is testnet only)
|
|
424
|
+
- [ ] Set `PRIVATE_KEY` and deployment keys only in server env, never in `VITE_` prefixed vars
|
|
425
|
+
- [ ] Audit your Solidity contract before mainnet
|
|
426
|
+
- [ ] Add `waitForTransactionReceipt` calls where confirmation matters
|
|
427
|
+
- [ ] Handle the `error` state from `useSmartAccount` visibly in your UI
|
|
428
|
+
- [ ] Add `.env` to `.gitignore`
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Contributing
|
|
433
|
+
|
|
434
|
+
Found a bug? Have a feature request? Contributions are welcome!
|
|
435
|
+
|
|
436
|
+
1. Fork the repo
|
|
437
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
438
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
439
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
440
|
+
5. Open a Pull Request
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Links
|
|
445
|
+
|
|
446
|
+
- **npm**: https://www.npmjs.com/package/erc4337-kit
|
|
447
|
+
- **GitHub**: https://github.com/atharvabaodhankar/erc4337-kit
|
|
448
|
+
- **Privy dashboard**: https://dashboard.privy.io
|
|
449
|
+
- **Pimlico dashboard**: https://dashboard.pimlico.io
|
|
450
|
+
- **Alchemy**: https://dashboard.alchemy.com
|
|
451
|
+
- **Polygon Amoy explorer**: https://amoy.polygonscan.com
|
|
452
|
+
- **ERC-4337 spec**: https://eips.ethereum.org/EIPS/eip-4337
|
|
453
|
+
|
|
454
|
+
---
|
|
155
455
|
|
|
156
|
-
|
|
456
|
+
## Support
|
|
157
457
|
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
- `sepolia` — Ethereum testnet
|
|
161
|
-
- `baseSepolia` — Base testnet
|
|
458
|
+
- 🐛 Issues: https://github.com/atharvabaodhankar/erc4337-kit/issues
|
|
459
|
+
- 💬 Discussions: https://github.com/atharvabaodhankar/erc4337-kit/discussions
|
|
162
460
|
|
|
163
461
|
---
|
|
164
462
|
|
|
165
463
|
## License
|
|
166
464
|
|
|
167
|
-
MIT
|
|
465
|
+
MIT © Atharva Baodhankar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erc4337-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Plug-and-play ERC-4337 Account Abstraction for React apps. Gasless txs, social login, smart accounts — without the complexity.",
|
|
5
5
|
"author": "Atharva Baodhankar",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,15 +20,13 @@
|
|
|
20
20
|
"smart-account",
|
|
21
21
|
"userops"
|
|
22
22
|
],
|
|
23
|
-
|
|
24
23
|
"type": "module",
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"module": "./dist/index.js",
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
28
26
|
"exports": {
|
|
29
27
|
".": {
|
|
30
|
-
"types":
|
|
31
|
-
"import":
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
32
30
|
"require": "./dist/index.cjs"
|
|
33
31
|
}
|
|
34
32
|
},
|
|
@@ -37,31 +35,27 @@
|
|
|
37
35
|
"src/contracts/BaseStorage.sol",
|
|
38
36
|
"README.md"
|
|
39
37
|
],
|
|
40
|
-
|
|
41
38
|
"scripts": {
|
|
42
|
-
"build":
|
|
43
|
-
"dev":
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
44
41
|
"prepublishOnly": "npm run build"
|
|
45
42
|
},
|
|
46
|
-
|
|
47
43
|
"peerDependencies": {
|
|
48
|
-
"react":
|
|
49
|
-
"react-dom":
|
|
50
|
-
"@privy-io/react-auth":">=3.0.0",
|
|
51
|
-
"@privy-io/wagmi":
|
|
52
|
-
"viem":
|
|
53
|
-
"wagmi":
|
|
54
|
-
"@tanstack/react-query":">=5.0.0"
|
|
44
|
+
"react": ">=18.0.0",
|
|
45
|
+
"react-dom": ">=18.0.0",
|
|
46
|
+
"@privy-io/react-auth": ">=3.0.0",
|
|
47
|
+
"@privy-io/wagmi": ">=4.0.0",
|
|
48
|
+
"viem": ">=2.0.0",
|
|
49
|
+
"wagmi": ">=3.0.0",
|
|
50
|
+
"@tanstack/react-query": ">=5.0.0"
|
|
55
51
|
},
|
|
56
|
-
|
|
57
52
|
"dependencies": {
|
|
58
53
|
"permissionless": "^0.3.4"
|
|
59
54
|
},
|
|
60
|
-
|
|
61
55
|
"devDependencies": {
|
|
62
|
-
"tsup":
|
|
63
|
-
"react":
|
|
64
|
-
"react-dom":
|
|
56
|
+
"tsup": "^8.0.0",
|
|
57
|
+
"react": "^18.2.0",
|
|
58
|
+
"react-dom": "^18.2.0",
|
|
65
59
|
"typescript": "^5.0.0"
|
|
66
60
|
}
|
|
67
61
|
}
|