├── .gitignore
├── README.md
├── abis
├── AnchorStateRegistry.json
├── Entrypoint.json
├── L1Block.json
├── SmartWallet.json
└── SmartWalletFactory.json
├── bun.lockb
├── docs
└── pages
│ ├── index.mdx
│ ├── keystore-basics.mdx
│ ├── maintaining-wallets.mdx
│ ├── references.mdx
│ ├── releases.mdx
│ ├── revoking-signers.mdx
│ ├── roadmap.mdx
│ ├── updating-keystore.mdx
│ ├── using-new-signers.mdx
│ └── web-services.mdx
├── dprint.json
├── generated.ts
├── keystore.bytecode
├── package-lock.json
├── package.json
├── scripts
├── change-owner.ts
├── create-p256-key.ts
├── get-account.ts
├── lib
│ ├── argparse.ts
│ └── client.ts
├── send-eth.ts
├── sync-keystore.ts
└── verify-p256-signature.ts
├── src
├── client.ts
├── config.ts
├── proofs
│ └── op-stack.ts
└── wallets
│ └── base-wallet
│ ├── config.ts
│ ├── contract.ts
│ ├── signers
│ ├── secp256k1
│ │ ├── calls.ts
│ │ ├── config-data.ts
│ │ ├── sign.ts
│ │ └── signatures.ts
│ └── webauthn
│ │ ├── calls.ts
│ │ ├── config-data.ts
│ │ ├── sign.ts
│ │ └── signatures.ts
│ └── user-op.ts
├── tsconfig.json
├── tsconfig.tsbuildinfo
├── vocs.config.ts
└── wagmi.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 |
15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
16 |
17 | # Runtime data
18 |
19 | pids
20 | _.pid
21 | _.seed
22 | \*.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 |
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 |
30 | coverage
31 | \*.lcov
32 |
33 | # nyc test coverage
34 |
35 | .nyc_output
36 |
37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38 |
39 | .grunt
40 |
41 | # Bower dependency directory (https://bower.io/)
42 |
43 | bower_components
44 |
45 | # node-waf configuration
46 |
47 | .lock-wscript
48 |
49 | # Compiled binary addons (https://nodejs.org/api/addons.html)
50 |
51 | build/Release
52 |
53 | # Dependency directories
54 |
55 | node_modules/
56 | jspm_packages/
57 |
58 | # Snowpack dependency directory (https://snowpack.dev/)
59 |
60 | web_modules/
61 |
62 | # TypeScript cache
63 |
64 | \*.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 |
68 | .npm
69 |
70 | # Optional eslint cache
71 |
72 | .eslintcache
73 |
74 | # Optional stylelint cache
75 |
76 | .stylelintcache
77 |
78 | # Microbundle cache
79 |
80 | .rpt2_cache/
81 | .rts2_cache_cjs/
82 | .rts2_cache_es/
83 | .rts2_cache_umd/
84 |
85 | # Optional REPL history
86 |
87 | .node_repl_history
88 |
89 | # Output of 'npm pack'
90 |
91 | \*.tgz
92 |
93 | # Yarn Integrity file
94 |
95 | .yarn-integrity
96 |
97 | # dotenv environment variable files
98 |
99 | .env
100 | .env.development.local
101 | .env.test.local
102 | .env.production.local
103 | .env.local
104 |
105 | # parcel-bundler cache (https://parceljs.org/)
106 |
107 | .cache
108 | .parcel-cache
109 |
110 | # Next.js build output
111 |
112 | .next
113 | out
114 |
115 | # Nuxt.js build / generate output
116 |
117 | .nuxt
118 | dist
119 |
120 | # Gatsby files
121 |
122 | .cache/
123 |
124 | # Comment in the public line in if your project uses Gatsby and not Next.js
125 |
126 | # https://nextjs.org/blog/next-9-1#public-directory-support
127 |
128 | # public
129 |
130 | # vuepress build output
131 |
132 | .vuepress/dist
133 |
134 | # vuepress v2.x temp and cache directory
135 |
136 | .temp
137 | .cache
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.\*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Keyspace Client Library
2 |
3 | A Keyspace client library implemented in TypeScript. This client is also the basis for the [Keyspace documentation](https://docs.key.space/).
4 |
5 | ## Install Dependencies
6 | ```bash
7 | bun install
8 | ```
9 |
10 | ## Create Private Keys
11 |
12 | ### secp256k1 (Ethereum EOA)
13 |
14 | ```bash
15 | cast wallet new
16 | ```
17 |
18 | ### P256 (secp256r1, Passkeys)
19 |
20 | ```bash
21 | bun run scripts/create-p256-key.ts
22 | ```
23 |
24 | ## Configuration
25 |
26 | `bun` automatically loads environment variables from a `.env` file. Create a `.env` file in the root of the project.
27 |
28 | ```bash
29 | touch .env
30 | ```
31 |
32 | | Environment Variable | Description |
33 | | --- | --- |
34 | | RPC_URL | Ethereum RPC URL for general RPC calls |
35 | | BUNDLER_RPC_URL | Ethereum RPC URL for ERC-4337 calls |
36 | | KEYSPACE_RPC_URL | Keyspace RPC URL |
37 | | RECOVERY_RPC_URL | Recovery Service RPC URL |
38 |
39 | ## Scripts
40 |
41 | ### Get Account
42 | ```bash
43 | bun run scripts/get-account.ts
44 | ```
45 |
46 | | Argument | Environment Variable | Description |
47 | | --- | --- | --- |
48 | | --private-key | PRIVATE_KEY | secp256k1 private key or P256 JWK |
49 | | --signature-type | | secp256k1 (default) or webauthn |
50 |
51 | ### Send ETH
52 | ```bash
53 | bun run scripts/send-eth.ts
54 | ```
55 |
56 | | Argument | Environment Variable | Description |
57 | | --- | --- | --- |
58 | | --account | | The account of the keystore wallet to send from |
59 | | --owner-index | | The index of the owner (default: 0) |
60 | | --initial-config-data | | The initial config data needed to deploy the wallet |
61 | | --private-key | PRIVATE_KEY | secp256k1 private key or P256 JWK |
62 | | --to | | The address to send to |
63 | | --signature-type | | secp256k1 (default) or webauthn |
64 |
65 | Make sure there's ETH in the account you're sending from. You can get the Ethereum address of the smart wallet by running `bun run scripts/get-account.ts`.
66 |
67 |
68 | ### Change Owner
69 | ```bash
70 | bun run scripts/change-owner.ts
71 | ```
72 |
73 | | Argument | Environment Variable | Description |
74 | | --- | --- | --- |
75 | | --account | | The account of the keystore wallet |
76 | | --owner-index | | The index of the owner (default: 0) |
77 | | --initial-config-data | | The initial config data needed to deploy the wallet |
78 | | --private-key | PRIVATE_KEY | Current private key of the owner |
79 | | --config-data | | Current config data for the keystore wallet (hex string) |
80 | | --owner-bytes | | The owner bytes to change in the keystore wallet |
81 | | --signature-type | | secp256k1 (default) or webauthn |
82 | | --remove | | Flag to remove the owner instead of adding (optional) |
83 |
84 | ## Build Documentation
85 |
86 | ```bash
87 | bun run docs:dev
88 | bun run docs:build
89 | ```
90 |
--------------------------------------------------------------------------------
/abis/AnchorStateRegistry.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "inputs": [
4 | {
5 | "internalType": "contract IDisputeGameFactory",
6 | "name": "_disputeGameFactory",
7 | "type": "address"
8 | }
9 | ],
10 | "stateMutability": "nonpayable",
11 | "type": "constructor"
12 | },
13 | {
14 | "inputs": [],
15 | "name": "InvalidGameStatus",
16 | "type": "error"
17 | },
18 | {
19 | "inputs": [],
20 | "name": "Unauthorized",
21 | "type": "error"
22 | },
23 | {
24 | "inputs": [],
25 | "name": "UnregisteredGame",
26 | "type": "error"
27 | },
28 | {
29 | "anonymous": false,
30 | "inputs": [
31 | {
32 | "indexed": false,
33 | "internalType": "uint8",
34 | "name": "version",
35 | "type": "uint8"
36 | }
37 | ],
38 | "name": "Initialized",
39 | "type": "event"
40 | },
41 | {
42 | "inputs": [
43 | {
44 | "internalType": "GameType",
45 | "name": "",
46 | "type": "uint32"
47 | }
48 | ],
49 | "name": "anchors",
50 | "outputs": [
51 | {
52 | "internalType": "Hash",
53 | "name": "root",
54 | "type": "bytes32"
55 | },
56 | {
57 | "internalType": "uint256",
58 | "name": "l2BlockNumber",
59 | "type": "uint256"
60 | }
61 | ],
62 | "stateMutability": "view",
63 | "type": "function"
64 | },
65 | {
66 | "inputs": [],
67 | "name": "disputeGameFactory",
68 | "outputs": [
69 | {
70 | "internalType": "contract IDisputeGameFactory",
71 | "name": "",
72 | "type": "address"
73 | }
74 | ],
75 | "stateMutability": "view",
76 | "type": "function"
77 | },
78 | {
79 | "inputs": [
80 | {
81 | "components": [
82 | {
83 | "internalType": "GameType",
84 | "name": "gameType",
85 | "type": "uint32"
86 | },
87 | {
88 | "components": [
89 | {
90 | "internalType": "Hash",
91 | "name": "root",
92 | "type": "bytes32"
93 | },
94 | {
95 | "internalType": "uint256",
96 | "name": "l2BlockNumber",
97 | "type": "uint256"
98 | }
99 | ],
100 | "internalType": "struct OutputRoot",
101 | "name": "outputRoot",
102 | "type": "tuple"
103 | }
104 | ],
105 | "internalType": "struct AnchorStateRegistry.StartingAnchorRoot[]",
106 | "name": "_startingAnchorRoots",
107 | "type": "tuple[]"
108 | },
109 | {
110 | "internalType": "contract SuperchainConfig",
111 | "name": "_superchainConfig",
112 | "type": "address"
113 | }
114 | ],
115 | "name": "initialize",
116 | "outputs": [],
117 | "stateMutability": "nonpayable",
118 | "type": "function"
119 | },
120 | {
121 | "inputs": [
122 | {
123 | "internalType": "contract IFaultDisputeGame",
124 | "name": "_game",
125 | "type": "address"
126 | }
127 | ],
128 | "name": "setAnchorState",
129 | "outputs": [],
130 | "stateMutability": "nonpayable",
131 | "type": "function"
132 | },
133 | {
134 | "inputs": [],
135 | "name": "superchainConfig",
136 | "outputs": [
137 | {
138 | "internalType": "contract SuperchainConfig",
139 | "name": "",
140 | "type": "address"
141 | }
142 | ],
143 | "stateMutability": "view",
144 | "type": "function"
145 | },
146 | {
147 | "inputs": [],
148 | "name": "tryUpdateAnchorState",
149 | "outputs": [],
150 | "stateMutability": "nonpayable",
151 | "type": "function"
152 | },
153 | {
154 | "inputs": [],
155 | "name": "version",
156 | "outputs": [
157 | {
158 | "internalType": "string",
159 | "name": "",
160 | "type": "string"
161 | }
162 | ],
163 | "stateMutability": "view",
164 | "type": "function"
165 | }
166 | ]
167 |
--------------------------------------------------------------------------------
/abis/Entrypoint.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "function",
4 | "name": "addStake",
5 | "inputs": [
6 | {
7 | "name": "_unstakeDelaySec",
8 | "type": "uint32",
9 | "internalType": "uint32"
10 | }
11 | ],
12 | "outputs": [],
13 | "stateMutability": "payable"
14 | },
15 | {
16 | "type": "function",
17 | "name": "balanceOf",
18 | "inputs": [
19 | {
20 | "name": "account",
21 | "type": "address",
22 | "internalType": "address"
23 | }
24 | ],
25 | "outputs": [
26 | {
27 | "name": "",
28 | "type": "uint256",
29 | "internalType": "uint256"
30 | }
31 | ],
32 | "stateMutability": "view"
33 | },
34 | {
35 | "type": "function",
36 | "name": "depositTo",
37 | "inputs": [
38 | {
39 | "name": "account",
40 | "type": "address",
41 | "internalType": "address"
42 | }
43 | ],
44 | "outputs": [],
45 | "stateMutability": "payable"
46 | },
47 | {
48 | "type": "function",
49 | "name": "getDepositInfo",
50 | "inputs": [
51 | {
52 | "name": "account",
53 | "type": "address",
54 | "internalType": "address"
55 | }
56 | ],
57 | "outputs": [
58 | {
59 | "name": "info",
60 | "type": "tuple",
61 | "internalType": "struct IStakeManager.DepositInfo",
62 | "components": [
63 | {
64 | "name": "deposit",
65 | "type": "uint112",
66 | "internalType": "uint112"
67 | },
68 | {
69 | "name": "staked",
70 | "type": "bool",
71 | "internalType": "bool"
72 | },
73 | {
74 | "name": "stake",
75 | "type": "uint112",
76 | "internalType": "uint112"
77 | },
78 | {
79 | "name": "unstakeDelaySec",
80 | "type": "uint32",
81 | "internalType": "uint32"
82 | },
83 | {
84 | "name": "withdrawTime",
85 | "type": "uint48",
86 | "internalType": "uint48"
87 | }
88 | ]
89 | }
90 | ],
91 | "stateMutability": "view"
92 | },
93 | {
94 | "type": "function",
95 | "name": "getNonce",
96 | "inputs": [
97 | {
98 | "name": "sender",
99 | "type": "address",
100 | "internalType": "address"
101 | },
102 | {
103 | "name": "key",
104 | "type": "uint192",
105 | "internalType": "uint192"
106 | }
107 | ],
108 | "outputs": [
109 | {
110 | "name": "nonce",
111 | "type": "uint256",
112 | "internalType": "uint256"
113 | }
114 | ],
115 | "stateMutability": "view"
116 | },
117 | {
118 | "type": "function",
119 | "name": "getSenderAddress",
120 | "inputs": [
121 | {
122 | "name": "initCode",
123 | "type": "bytes",
124 | "internalType": "bytes"
125 | }
126 | ],
127 | "outputs": [],
128 | "stateMutability": "nonpayable"
129 | },
130 | {
131 | "type": "function",
132 | "name": "getUserOpHash",
133 | "inputs": [
134 | {
135 | "name": "userOp",
136 | "type": "tuple",
137 | "internalType": "struct UserOperation",
138 | "components": [
139 | {
140 | "name": "sender",
141 | "type": "address",
142 | "internalType": "address"
143 | },
144 | {
145 | "name": "nonce",
146 | "type": "uint256",
147 | "internalType": "uint256"
148 | },
149 | {
150 | "name": "initCode",
151 | "type": "bytes",
152 | "internalType": "bytes"
153 | },
154 | {
155 | "name": "callData",
156 | "type": "bytes",
157 | "internalType": "bytes"
158 | },
159 | {
160 | "name": "callGasLimit",
161 | "type": "uint256",
162 | "internalType": "uint256"
163 | },
164 | {
165 | "name": "verificationGasLimit",
166 | "type": "uint256",
167 | "internalType": "uint256"
168 | },
169 | {
170 | "name": "preVerificationGas",
171 | "type": "uint256",
172 | "internalType": "uint256"
173 | },
174 | {
175 | "name": "maxFeePerGas",
176 | "type": "uint256",
177 | "internalType": "uint256"
178 | },
179 | {
180 | "name": "maxPriorityFeePerGas",
181 | "type": "uint256",
182 | "internalType": "uint256"
183 | },
184 | {
185 | "name": "paymasterAndData",
186 | "type": "bytes",
187 | "internalType": "bytes"
188 | },
189 | {
190 | "name": "signature",
191 | "type": "bytes",
192 | "internalType": "bytes"
193 | }
194 | ]
195 | }
196 | ],
197 | "outputs": [
198 | {
199 | "name": "",
200 | "type": "bytes32",
201 | "internalType": "bytes32"
202 | }
203 | ],
204 | "stateMutability": "view"
205 | },
206 | {
207 | "type": "function",
208 | "name": "handleAggregatedOps",
209 | "inputs": [
210 | {
211 | "name": "opsPerAggregator",
212 | "type": "tuple[]",
213 | "internalType": "struct IEntryPoint.UserOpsPerAggregator[]",
214 | "components": [
215 | {
216 | "name": "userOps",
217 | "type": "tuple[]",
218 | "internalType": "struct UserOperation[]",
219 | "components": [
220 | {
221 | "name": "sender",
222 | "type": "address",
223 | "internalType": "address"
224 | },
225 | {
226 | "name": "nonce",
227 | "type": "uint256",
228 | "internalType": "uint256"
229 | },
230 | {
231 | "name": "initCode",
232 | "type": "bytes",
233 | "internalType": "bytes"
234 | },
235 | {
236 | "name": "callData",
237 | "type": "bytes",
238 | "internalType": "bytes"
239 | },
240 | {
241 | "name": "callGasLimit",
242 | "type": "uint256",
243 | "internalType": "uint256"
244 | },
245 | {
246 | "name": "verificationGasLimit",
247 | "type": "uint256",
248 | "internalType": "uint256"
249 | },
250 | {
251 | "name": "preVerificationGas",
252 | "type": "uint256",
253 | "internalType": "uint256"
254 | },
255 | {
256 | "name": "maxFeePerGas",
257 | "type": "uint256",
258 | "internalType": "uint256"
259 | },
260 | {
261 | "name": "maxPriorityFeePerGas",
262 | "type": "uint256",
263 | "internalType": "uint256"
264 | },
265 | {
266 | "name": "paymasterAndData",
267 | "type": "bytes",
268 | "internalType": "bytes"
269 | },
270 | {
271 | "name": "signature",
272 | "type": "bytes",
273 | "internalType": "bytes"
274 | }
275 | ]
276 | },
277 | {
278 | "name": "aggregator",
279 | "type": "address",
280 | "internalType": "contract IAggregator"
281 | },
282 | {
283 | "name": "signature",
284 | "type": "bytes",
285 | "internalType": "bytes"
286 | }
287 | ]
288 | },
289 | {
290 | "name": "beneficiary",
291 | "type": "address",
292 | "internalType": "address payable"
293 | }
294 | ],
295 | "outputs": [],
296 | "stateMutability": "nonpayable"
297 | },
298 | {
299 | "type": "function",
300 | "name": "handleOps",
301 | "inputs": [
302 | {
303 | "name": "ops",
304 | "type": "tuple[]",
305 | "internalType": "struct UserOperation[]",
306 | "components": [
307 | {
308 | "name": "sender",
309 | "type": "address",
310 | "internalType": "address"
311 | },
312 | {
313 | "name": "nonce",
314 | "type": "uint256",
315 | "internalType": "uint256"
316 | },
317 | {
318 | "name": "initCode",
319 | "type": "bytes",
320 | "internalType": "bytes"
321 | },
322 | {
323 | "name": "callData",
324 | "type": "bytes",
325 | "internalType": "bytes"
326 | },
327 | {
328 | "name": "callGasLimit",
329 | "type": "uint256",
330 | "internalType": "uint256"
331 | },
332 | {
333 | "name": "verificationGasLimit",
334 | "type": "uint256",
335 | "internalType": "uint256"
336 | },
337 | {
338 | "name": "preVerificationGas",
339 | "type": "uint256",
340 | "internalType": "uint256"
341 | },
342 | {
343 | "name": "maxFeePerGas",
344 | "type": "uint256",
345 | "internalType": "uint256"
346 | },
347 | {
348 | "name": "maxPriorityFeePerGas",
349 | "type": "uint256",
350 | "internalType": "uint256"
351 | },
352 | {
353 | "name": "paymasterAndData",
354 | "type": "bytes",
355 | "internalType": "bytes"
356 | },
357 | {
358 | "name": "signature",
359 | "type": "bytes",
360 | "internalType": "bytes"
361 | }
362 | ]
363 | },
364 | {
365 | "name": "beneficiary",
366 | "type": "address",
367 | "internalType": "address payable"
368 | }
369 | ],
370 | "outputs": [],
371 | "stateMutability": "nonpayable"
372 | },
373 | {
374 | "type": "function",
375 | "name": "incrementNonce",
376 | "inputs": [
377 | {
378 | "name": "key",
379 | "type": "uint192",
380 | "internalType": "uint192"
381 | }
382 | ],
383 | "outputs": [],
384 | "stateMutability": "nonpayable"
385 | },
386 | {
387 | "type": "function",
388 | "name": "simulateHandleOp",
389 | "inputs": [
390 | {
391 | "name": "op",
392 | "type": "tuple",
393 | "internalType": "struct UserOperation",
394 | "components": [
395 | {
396 | "name": "sender",
397 | "type": "address",
398 | "internalType": "address"
399 | },
400 | {
401 | "name": "nonce",
402 | "type": "uint256",
403 | "internalType": "uint256"
404 | },
405 | {
406 | "name": "initCode",
407 | "type": "bytes",
408 | "internalType": "bytes"
409 | },
410 | {
411 | "name": "callData",
412 | "type": "bytes",
413 | "internalType": "bytes"
414 | },
415 | {
416 | "name": "callGasLimit",
417 | "type": "uint256",
418 | "internalType": "uint256"
419 | },
420 | {
421 | "name": "verificationGasLimit",
422 | "type": "uint256",
423 | "internalType": "uint256"
424 | },
425 | {
426 | "name": "preVerificationGas",
427 | "type": "uint256",
428 | "internalType": "uint256"
429 | },
430 | {
431 | "name": "maxFeePerGas",
432 | "type": "uint256",
433 | "internalType": "uint256"
434 | },
435 | {
436 | "name": "maxPriorityFeePerGas",
437 | "type": "uint256",
438 | "internalType": "uint256"
439 | },
440 | {
441 | "name": "paymasterAndData",
442 | "type": "bytes",
443 | "internalType": "bytes"
444 | },
445 | {
446 | "name": "signature",
447 | "type": "bytes",
448 | "internalType": "bytes"
449 | }
450 | ]
451 | },
452 | {
453 | "name": "target",
454 | "type": "address",
455 | "internalType": "address"
456 | },
457 | {
458 | "name": "targetCallData",
459 | "type": "bytes",
460 | "internalType": "bytes"
461 | }
462 | ],
463 | "outputs": [],
464 | "stateMutability": "nonpayable"
465 | },
466 | {
467 | "type": "function",
468 | "name": "simulateValidation",
469 | "inputs": [
470 | {
471 | "name": "userOp",
472 | "type": "tuple",
473 | "internalType": "struct UserOperation",
474 | "components": [
475 | {
476 | "name": "sender",
477 | "type": "address",
478 | "internalType": "address"
479 | },
480 | {
481 | "name": "nonce",
482 | "type": "uint256",
483 | "internalType": "uint256"
484 | },
485 | {
486 | "name": "initCode",
487 | "type": "bytes",
488 | "internalType": "bytes"
489 | },
490 | {
491 | "name": "callData",
492 | "type": "bytes",
493 | "internalType": "bytes"
494 | },
495 | {
496 | "name": "callGasLimit",
497 | "type": "uint256",
498 | "internalType": "uint256"
499 | },
500 | {
501 | "name": "verificationGasLimit",
502 | "type": "uint256",
503 | "internalType": "uint256"
504 | },
505 | {
506 | "name": "preVerificationGas",
507 | "type": "uint256",
508 | "internalType": "uint256"
509 | },
510 | {
511 | "name": "maxFeePerGas",
512 | "type": "uint256",
513 | "internalType": "uint256"
514 | },
515 | {
516 | "name": "maxPriorityFeePerGas",
517 | "type": "uint256",
518 | "internalType": "uint256"
519 | },
520 | {
521 | "name": "paymasterAndData",
522 | "type": "bytes",
523 | "internalType": "bytes"
524 | },
525 | {
526 | "name": "signature",
527 | "type": "bytes",
528 | "internalType": "bytes"
529 | }
530 | ]
531 | }
532 | ],
533 | "outputs": [],
534 | "stateMutability": "nonpayable"
535 | },
536 | {
537 | "type": "function",
538 | "name": "unlockStake",
539 | "inputs": [],
540 | "outputs": [],
541 | "stateMutability": "nonpayable"
542 | },
543 | {
544 | "type": "function",
545 | "name": "withdrawStake",
546 | "inputs": [
547 | {
548 | "name": "withdrawAddress",
549 | "type": "address",
550 | "internalType": "address payable"
551 | }
552 | ],
553 | "outputs": [],
554 | "stateMutability": "nonpayable"
555 | },
556 | {
557 | "type": "function",
558 | "name": "withdrawTo",
559 | "inputs": [
560 | {
561 | "name": "withdrawAddress",
562 | "type": "address",
563 | "internalType": "address payable"
564 | },
565 | {
566 | "name": "withdrawAmount",
567 | "type": "uint256",
568 | "internalType": "uint256"
569 | }
570 | ],
571 | "outputs": [],
572 | "stateMutability": "nonpayable"
573 | },
574 | {
575 | "type": "event",
576 | "name": "AccountDeployed",
577 | "inputs": [
578 | {
579 | "name": "userOpHash",
580 | "type": "bytes32",
581 | "indexed": true,
582 | "internalType": "bytes32"
583 | },
584 | {
585 | "name": "sender",
586 | "type": "address",
587 | "indexed": true,
588 | "internalType": "address"
589 | },
590 | {
591 | "name": "factory",
592 | "type": "address",
593 | "indexed": false,
594 | "internalType": "address"
595 | },
596 | {
597 | "name": "paymaster",
598 | "type": "address",
599 | "indexed": false,
600 | "internalType": "address"
601 | }
602 | ],
603 | "anonymous": false
604 | },
605 | {
606 | "type": "event",
607 | "name": "BeforeExecution",
608 | "inputs": [],
609 | "anonymous": false
610 | },
611 | {
612 | "type": "event",
613 | "name": "Deposited",
614 | "inputs": [
615 | {
616 | "name": "account",
617 | "type": "address",
618 | "indexed": true,
619 | "internalType": "address"
620 | },
621 | {
622 | "name": "totalDeposit",
623 | "type": "uint256",
624 | "indexed": false,
625 | "internalType": "uint256"
626 | }
627 | ],
628 | "anonymous": false
629 | },
630 | {
631 | "type": "event",
632 | "name": "SignatureAggregatorChanged",
633 | "inputs": [
634 | {
635 | "name": "aggregator",
636 | "type": "address",
637 | "indexed": true,
638 | "internalType": "address"
639 | }
640 | ],
641 | "anonymous": false
642 | },
643 | {
644 | "type": "event",
645 | "name": "StakeLocked",
646 | "inputs": [
647 | {
648 | "name": "account",
649 | "type": "address",
650 | "indexed": true,
651 | "internalType": "address"
652 | },
653 | {
654 | "name": "totalStaked",
655 | "type": "uint256",
656 | "indexed": false,
657 | "internalType": "uint256"
658 | },
659 | {
660 | "name": "unstakeDelaySec",
661 | "type": "uint256",
662 | "indexed": false,
663 | "internalType": "uint256"
664 | }
665 | ],
666 | "anonymous": false
667 | },
668 | {
669 | "type": "event",
670 | "name": "StakeUnlocked",
671 | "inputs": [
672 | {
673 | "name": "account",
674 | "type": "address",
675 | "indexed": true,
676 | "internalType": "address"
677 | },
678 | {
679 | "name": "withdrawTime",
680 | "type": "uint256",
681 | "indexed": false,
682 | "internalType": "uint256"
683 | }
684 | ],
685 | "anonymous": false
686 | },
687 | {
688 | "type": "event",
689 | "name": "StakeWithdrawn",
690 | "inputs": [
691 | {
692 | "name": "account",
693 | "type": "address",
694 | "indexed": true,
695 | "internalType": "address"
696 | },
697 | {
698 | "name": "withdrawAddress",
699 | "type": "address",
700 | "indexed": false,
701 | "internalType": "address"
702 | },
703 | {
704 | "name": "amount",
705 | "type": "uint256",
706 | "indexed": false,
707 | "internalType": "uint256"
708 | }
709 | ],
710 | "anonymous": false
711 | },
712 | {
713 | "type": "event",
714 | "name": "UserOperationEvent",
715 | "inputs": [
716 | {
717 | "name": "userOpHash",
718 | "type": "bytes32",
719 | "indexed": true,
720 | "internalType": "bytes32"
721 | },
722 | {
723 | "name": "sender",
724 | "type": "address",
725 | "indexed": true,
726 | "internalType": "address"
727 | },
728 | {
729 | "name": "paymaster",
730 | "type": "address",
731 | "indexed": true,
732 | "internalType": "address"
733 | },
734 | {
735 | "name": "nonce",
736 | "type": "uint256",
737 | "indexed": false,
738 | "internalType": "uint256"
739 | },
740 | {
741 | "name": "success",
742 | "type": "bool",
743 | "indexed": false,
744 | "internalType": "bool"
745 | },
746 | {
747 | "name": "actualGasCost",
748 | "type": "uint256",
749 | "indexed": false,
750 | "internalType": "uint256"
751 | },
752 | {
753 | "name": "actualGasUsed",
754 | "type": "uint256",
755 | "indexed": false,
756 | "internalType": "uint256"
757 | }
758 | ],
759 | "anonymous": false
760 | },
761 | {
762 | "type": "event",
763 | "name": "UserOperationRevertReason",
764 | "inputs": [
765 | {
766 | "name": "userOpHash",
767 | "type": "bytes32",
768 | "indexed": true,
769 | "internalType": "bytes32"
770 | },
771 | {
772 | "name": "sender",
773 | "type": "address",
774 | "indexed": true,
775 | "internalType": "address"
776 | },
777 | {
778 | "name": "nonce",
779 | "type": "uint256",
780 | "indexed": false,
781 | "internalType": "uint256"
782 | },
783 | {
784 | "name": "revertReason",
785 | "type": "bytes",
786 | "indexed": false,
787 | "internalType": "bytes"
788 | }
789 | ],
790 | "anonymous": false
791 | },
792 | {
793 | "type": "event",
794 | "name": "Withdrawn",
795 | "inputs": [
796 | {
797 | "name": "account",
798 | "type": "address",
799 | "indexed": true,
800 | "internalType": "address"
801 | },
802 | {
803 | "name": "withdrawAddress",
804 | "type": "address",
805 | "indexed": false,
806 | "internalType": "address"
807 | },
808 | {
809 | "name": "amount",
810 | "type": "uint256",
811 | "indexed": false,
812 | "internalType": "uint256"
813 | }
814 | ],
815 | "anonymous": false
816 | },
817 | {
818 | "type": "error",
819 | "name": "ExecutionResult",
820 | "inputs": [
821 | {
822 | "name": "preOpGas",
823 | "type": "uint256",
824 | "internalType": "uint256"
825 | },
826 | {
827 | "name": "paid",
828 | "type": "uint256",
829 | "internalType": "uint256"
830 | },
831 | {
832 | "name": "validAfter",
833 | "type": "uint48",
834 | "internalType": "uint48"
835 | },
836 | {
837 | "name": "validUntil",
838 | "type": "uint48",
839 | "internalType": "uint48"
840 | },
841 | {
842 | "name": "targetSuccess",
843 | "type": "bool",
844 | "internalType": "bool"
845 | },
846 | {
847 | "name": "targetResult",
848 | "type": "bytes",
849 | "internalType": "bytes"
850 | }
851 | ]
852 | },
853 | {
854 | "type": "error",
855 | "name": "FailedOp",
856 | "inputs": [
857 | {
858 | "name": "opIndex",
859 | "type": "uint256",
860 | "internalType": "uint256"
861 | },
862 | {
863 | "name": "reason",
864 | "type": "string",
865 | "internalType": "string"
866 | }
867 | ]
868 | },
869 | {
870 | "type": "error",
871 | "name": "SenderAddressResult",
872 | "inputs": [
873 | {
874 | "name": "sender",
875 | "type": "address",
876 | "internalType": "address"
877 | }
878 | ]
879 | },
880 | {
881 | "type": "error",
882 | "name": "SignatureValidationFailed",
883 | "inputs": [
884 | {
885 | "name": "aggregator",
886 | "type": "address",
887 | "internalType": "address"
888 | }
889 | ]
890 | },
891 | {
892 | "type": "error",
893 | "name": "ValidationResult",
894 | "inputs": [
895 | {
896 | "name": "returnInfo",
897 | "type": "tuple",
898 | "internalType": "struct IEntryPoint.ReturnInfo",
899 | "components": [
900 | {
901 | "name": "preOpGas",
902 | "type": "uint256",
903 | "internalType": "uint256"
904 | },
905 | {
906 | "name": "prefund",
907 | "type": "uint256",
908 | "internalType": "uint256"
909 | },
910 | {
911 | "name": "sigFailed",
912 | "type": "bool",
913 | "internalType": "bool"
914 | },
915 | {
916 | "name": "validAfter",
917 | "type": "uint48",
918 | "internalType": "uint48"
919 | },
920 | {
921 | "name": "validUntil",
922 | "type": "uint48",
923 | "internalType": "uint48"
924 | },
925 | {
926 | "name": "paymasterContext",
927 | "type": "bytes",
928 | "internalType": "bytes"
929 | }
930 | ]
931 | },
932 | {
933 | "name": "senderInfo",
934 | "type": "tuple",
935 | "internalType": "struct IStakeManager.StakeInfo",
936 | "components": [
937 | {
938 | "name": "stake",
939 | "type": "uint256",
940 | "internalType": "uint256"
941 | },
942 | {
943 | "name": "unstakeDelaySec",
944 | "type": "uint256",
945 | "internalType": "uint256"
946 | }
947 | ]
948 | },
949 | {
950 | "name": "factoryInfo",
951 | "type": "tuple",
952 | "internalType": "struct IStakeManager.StakeInfo",
953 | "components": [
954 | {
955 | "name": "stake",
956 | "type": "uint256",
957 | "internalType": "uint256"
958 | },
959 | {
960 | "name": "unstakeDelaySec",
961 | "type": "uint256",
962 | "internalType": "uint256"
963 | }
964 | ]
965 | },
966 | {
967 | "name": "paymasterInfo",
968 | "type": "tuple",
969 | "internalType": "struct IStakeManager.StakeInfo",
970 | "components": [
971 | {
972 | "name": "stake",
973 | "type": "uint256",
974 | "internalType": "uint256"
975 | },
976 | {
977 | "name": "unstakeDelaySec",
978 | "type": "uint256",
979 | "internalType": "uint256"
980 | }
981 | ]
982 | }
983 | ]
984 | },
985 | {
986 | "type": "error",
987 | "name": "ValidationResultWithAggregation",
988 | "inputs": [
989 | {
990 | "name": "returnInfo",
991 | "type": "tuple",
992 | "internalType": "struct IEntryPoint.ReturnInfo",
993 | "components": [
994 | {
995 | "name": "preOpGas",
996 | "type": "uint256",
997 | "internalType": "uint256"
998 | },
999 | {
1000 | "name": "prefund",
1001 | "type": "uint256",
1002 | "internalType": "uint256"
1003 | },
1004 | {
1005 | "name": "sigFailed",
1006 | "type": "bool",
1007 | "internalType": "bool"
1008 | },
1009 | {
1010 | "name": "validAfter",
1011 | "type": "uint48",
1012 | "internalType": "uint48"
1013 | },
1014 | {
1015 | "name": "validUntil",
1016 | "type": "uint48",
1017 | "internalType": "uint48"
1018 | },
1019 | {
1020 | "name": "paymasterContext",
1021 | "type": "bytes",
1022 | "internalType": "bytes"
1023 | }
1024 | ]
1025 | },
1026 | {
1027 | "name": "senderInfo",
1028 | "type": "tuple",
1029 | "internalType": "struct IStakeManager.StakeInfo",
1030 | "components": [
1031 | {
1032 | "name": "stake",
1033 | "type": "uint256",
1034 | "internalType": "uint256"
1035 | },
1036 | {
1037 | "name": "unstakeDelaySec",
1038 | "type": "uint256",
1039 | "internalType": "uint256"
1040 | }
1041 | ]
1042 | },
1043 | {
1044 | "name": "factoryInfo",
1045 | "type": "tuple",
1046 | "internalType": "struct IStakeManager.StakeInfo",
1047 | "components": [
1048 | {
1049 | "name": "stake",
1050 | "type": "uint256",
1051 | "internalType": "uint256"
1052 | },
1053 | {
1054 | "name": "unstakeDelaySec",
1055 | "type": "uint256",
1056 | "internalType": "uint256"
1057 | }
1058 | ]
1059 | },
1060 | {
1061 | "name": "paymasterInfo",
1062 | "type": "tuple",
1063 | "internalType": "struct IStakeManager.StakeInfo",
1064 | "components": [
1065 | {
1066 | "name": "stake",
1067 | "type": "uint256",
1068 | "internalType": "uint256"
1069 | },
1070 | {
1071 | "name": "unstakeDelaySec",
1072 | "type": "uint256",
1073 | "internalType": "uint256"
1074 | }
1075 | ]
1076 | },
1077 | {
1078 | "name": "aggregatorInfo",
1079 | "type": "tuple",
1080 | "internalType": "struct IEntryPoint.AggregatorStakeInfo",
1081 | "components": [
1082 | {
1083 | "name": "aggregator",
1084 | "type": "address",
1085 | "internalType": "address"
1086 | },
1087 | {
1088 | "name": "stakeInfo",
1089 | "type": "tuple",
1090 | "internalType": "struct IStakeManager.StakeInfo",
1091 | "components": [
1092 | {
1093 | "name": "stake",
1094 | "type": "uint256",
1095 | "internalType": "uint256"
1096 | },
1097 | {
1098 | "name": "unstakeDelaySec",
1099 | "type": "uint256",
1100 | "internalType": "uint256"
1101 | }
1102 | ]
1103 | }
1104 | ]
1105 | }
1106 | ]
1107 | }
1108 | ]
1109 |
--------------------------------------------------------------------------------
/abis/L1Block.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "inputs": [],
4 | "name": "DEPOSITOR_ACCOUNT",
5 | "outputs": [
6 | {
7 | "internalType": "address",
8 | "name": "",
9 | "type": "address"
10 | }
11 | ],
12 | "stateMutability": "view",
13 | "type": "function"
14 | },
15 | {
16 | "inputs": [],
17 | "name": "basefee",
18 | "outputs": [
19 | {
20 | "internalType": "uint256",
21 | "name": "",
22 | "type": "uint256"
23 | }
24 | ],
25 | "stateMutability": "view",
26 | "type": "function"
27 | },
28 | {
29 | "inputs": [],
30 | "name": "batcherHash",
31 | "outputs": [
32 | {
33 | "internalType": "bytes32",
34 | "name": "",
35 | "type": "bytes32"
36 | }
37 | ],
38 | "stateMutability": "view",
39 | "type": "function"
40 | },
41 | {
42 | "inputs": [],
43 | "name": "hash",
44 | "outputs": [
45 | {
46 | "internalType": "bytes32",
47 | "name": "",
48 | "type": "bytes32"
49 | }
50 | ],
51 | "stateMutability": "view",
52 | "type": "function"
53 | },
54 | {
55 | "inputs": [],
56 | "name": "l1FeeOverhead",
57 | "outputs": [
58 | {
59 | "internalType": "uint256",
60 | "name": "",
61 | "type": "uint256"
62 | }
63 | ],
64 | "stateMutability": "view",
65 | "type": "function"
66 | },
67 | {
68 | "inputs": [],
69 | "name": "l1FeeScalar",
70 | "outputs": [
71 | {
72 | "internalType": "uint256",
73 | "name": "",
74 | "type": "uint256"
75 | }
76 | ],
77 | "stateMutability": "view",
78 | "type": "function"
79 | },
80 | {
81 | "inputs": [],
82 | "name": "number",
83 | "outputs": [
84 | {
85 | "internalType": "uint64",
86 | "name": "",
87 | "type": "uint64"
88 | }
89 | ],
90 | "stateMutability": "view",
91 | "type": "function"
92 | },
93 | {
94 | "inputs": [],
95 | "name": "sequenceNumber",
96 | "outputs": [
97 | {
98 | "internalType": "uint64",
99 | "name": "",
100 | "type": "uint64"
101 | }
102 | ],
103 | "stateMutability": "view",
104 | "type": "function"
105 | },
106 | {
107 | "inputs": [
108 | {
109 | "internalType": "uint64",
110 | "name": "_number",
111 | "type": "uint64"
112 | },
113 | {
114 | "internalType": "uint64",
115 | "name": "_timestamp",
116 | "type": "uint64"
117 | },
118 | {
119 | "internalType": "uint256",
120 | "name": "_basefee",
121 | "type": "uint256"
122 | },
123 | {
124 | "internalType": "bytes32",
125 | "name": "_hash",
126 | "type": "bytes32"
127 | },
128 | {
129 | "internalType": "uint64",
130 | "name": "_sequenceNumber",
131 | "type": "uint64"
132 | },
133 | {
134 | "internalType": "bytes32",
135 | "name": "_batcherHash",
136 | "type": "bytes32"
137 | },
138 | {
139 | "internalType": "uint256",
140 | "name": "_l1FeeOverhead",
141 | "type": "uint256"
142 | },
143 | {
144 | "internalType": "uint256",
145 | "name": "_l1FeeScalar",
146 | "type": "uint256"
147 | }
148 | ],
149 | "name": "setL1BlockValues",
150 | "outputs": [],
151 | "stateMutability": "nonpayable",
152 | "type": "function"
153 | },
154 | {
155 | "inputs": [],
156 | "name": "timestamp",
157 | "outputs": [
158 | {
159 | "internalType": "uint64",
160 | "name": "",
161 | "type": "uint64"
162 | }
163 | ],
164 | "stateMutability": "view",
165 | "type": "function"
166 | },
167 | {
168 | "inputs": [],
169 | "name": "version",
170 | "outputs": [
171 | {
172 | "internalType": "string",
173 | "name": "",
174 | "type": "string"
175 | }
176 | ],
177 | "stateMutability": "view",
178 | "type": "function"
179 | }
180 | ]
181 |
--------------------------------------------------------------------------------
/abis/SmartWallet.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "constructor",
4 | "inputs": [
5 | {
6 | "name": "masterChainId",
7 | "type": "uint256",
8 | "internalType": "uint256"
9 | }
10 | ],
11 | "stateMutability": "nonpayable"
12 | },
13 | {
14 | "type": "fallback",
15 | "stateMutability": "payable"
16 | },
17 | {
18 | "type": "receive",
19 | "stateMutability": "payable"
20 | },
21 | {
22 | "type": "function",
23 | "name": "REPLAYABLE_NONCE_KEY",
24 | "inputs": [],
25 | "outputs": [
26 | {
27 | "name": "",
28 | "type": "uint256",
29 | "internalType": "uint256"
30 | }
31 | ],
32 | "stateMutability": "view"
33 | },
34 | {
35 | "type": "function",
36 | "name": "canSkipChainIdValidation",
37 | "inputs": [
38 | {
39 | "name": "functionSelector",
40 | "type": "bytes4",
41 | "internalType": "bytes4"
42 | }
43 | ],
44 | "outputs": [
45 | {
46 | "name": "",
47 | "type": "bool",
48 | "internalType": "bool"
49 | }
50 | ],
51 | "stateMutability": "pure"
52 | },
53 | {
54 | "type": "function",
55 | "name": "syncConfig",
56 | "inputs": [
57 | {
58 | "name": "newConfirmedConfig",
59 | "type": "tuple",
60 | "internalType": "struct ConfigLib.Config",
61 | "components": [
62 | {
63 | "name": "nonce",
64 | "type": "uint256",
65 | "internalType": "uint256"
66 | },
67 | {
68 | "name": "data",
69 | "type": "bytes",
70 | "internalType": "bytes"
71 | }
72 | ]
73 | },
74 | {
75 | "name": "keystoreProof",
76 | "type": "bytes",
77 | "internalType": "bytes"
78 | }
79 | ],
80 | "outputs": [],
81 | "stateMutability": "nonpayable"
82 | },
83 | {
84 | "type": "function",
85 | "name": "domainSeparator",
86 | "inputs": [],
87 | "outputs": [
88 | {
89 | "name": "",
90 | "type": "bytes32",
91 | "internalType": "bytes32"
92 | }
93 | ],
94 | "stateMutability": "view"
95 | },
96 | {
97 | "type": "function",
98 | "name": "eip712Domain",
99 | "inputs": [],
100 | "outputs": [
101 | {
102 | "name": "fields",
103 | "type": "bytes1",
104 | "internalType": "bytes1"
105 | },
106 | {
107 | "name": "name",
108 | "type": "string",
109 | "internalType": "string"
110 | },
111 | {
112 | "name": "version",
113 | "type": "string",
114 | "internalType": "string"
115 | },
116 | {
117 | "name": "chainId",
118 | "type": "uint256",
119 | "internalType": "uint256"
120 | },
121 | {
122 | "name": "verifyingContract",
123 | "type": "address",
124 | "internalType": "address"
125 | },
126 | {
127 | "name": "salt",
128 | "type": "bytes32",
129 | "internalType": "bytes32"
130 | },
131 | {
132 | "name": "extensions",
133 | "type": "uint256[]",
134 | "internalType": "uint256[]"
135 | }
136 | ],
137 | "stateMutability": "view"
138 | },
139 | {
140 | "type": "function",
141 | "name": "entryPoint",
142 | "inputs": [],
143 | "outputs": [
144 | {
145 | "name": "",
146 | "type": "address",
147 | "internalType": "address"
148 | }
149 | ],
150 | "stateMutability": "view"
151 | },
152 | {
153 | "type": "function",
154 | "name": "execute",
155 | "inputs": [
156 | {
157 | "name": "target",
158 | "type": "address",
159 | "internalType": "address"
160 | },
161 | {
162 | "name": "value",
163 | "type": "uint256",
164 | "internalType": "uint256"
165 | },
166 | {
167 | "name": "data",
168 | "type": "bytes",
169 | "internalType": "bytes"
170 | }
171 | ],
172 | "outputs": [],
173 | "stateMutability": "payable"
174 | },
175 | {
176 | "type": "function",
177 | "name": "executeBatch",
178 | "inputs": [
179 | {
180 | "name": "calls",
181 | "type": "tuple[]",
182 | "internalType": "struct CoinbaseSmartWallet.Call[]",
183 | "components": [
184 | {
185 | "name": "target",
186 | "type": "address",
187 | "internalType": "address"
188 | },
189 | {
190 | "name": "value",
191 | "type": "uint256",
192 | "internalType": "uint256"
193 | },
194 | {
195 | "name": "data",
196 | "type": "bytes",
197 | "internalType": "bytes"
198 | }
199 | ]
200 | }
201 | ],
202 | "outputs": [],
203 | "stateMutability": "payable"
204 | },
205 | {
206 | "type": "function",
207 | "name": "executeWithoutChainIdValidation",
208 | "inputs": [
209 | {
210 | "name": "calls",
211 | "type": "bytes[]",
212 | "internalType": "bytes[]"
213 | }
214 | ],
215 | "outputs": [],
216 | "stateMutability": "payable"
217 | },
218 | {
219 | "type": "function",
220 | "name": "getUserOpHashWithoutChainId",
221 | "inputs": [
222 | {
223 | "name": "userOp",
224 | "type": "tuple",
225 | "internalType": "struct UserOperation",
226 | "components": [
227 | {
228 | "name": "sender",
229 | "type": "address",
230 | "internalType": "address"
231 | },
232 | {
233 | "name": "nonce",
234 | "type": "uint256",
235 | "internalType": "uint256"
236 | },
237 | {
238 | "name": "initCode",
239 | "type": "bytes",
240 | "internalType": "bytes"
241 | },
242 | {
243 | "name": "callData",
244 | "type": "bytes",
245 | "internalType": "bytes"
246 | },
247 | {
248 | "name": "callGasLimit",
249 | "type": "uint256",
250 | "internalType": "uint256"
251 | },
252 | {
253 | "name": "verificationGasLimit",
254 | "type": "uint256",
255 | "internalType": "uint256"
256 | },
257 | {
258 | "name": "preVerificationGas",
259 | "type": "uint256",
260 | "internalType": "uint256"
261 | },
262 | {
263 | "name": "maxFeePerGas",
264 | "type": "uint256",
265 | "internalType": "uint256"
266 | },
267 | {
268 | "name": "maxPriorityFeePerGas",
269 | "type": "uint256",
270 | "internalType": "uint256"
271 | },
272 | {
273 | "name": "paymasterAndData",
274 | "type": "bytes",
275 | "internalType": "bytes"
276 | },
277 | {
278 | "name": "signature",
279 | "type": "bytes",
280 | "internalType": "bytes"
281 | }
282 | ]
283 | }
284 | ],
285 | "outputs": [
286 | {
287 | "name": "",
288 | "type": "bytes32",
289 | "internalType": "bytes32"
290 | }
291 | ],
292 | "stateMutability": "view"
293 | },
294 | {
295 | "type": "function",
296 | "name": "hookIsNewConfigValid",
297 | "inputs": [
298 | {
299 | "name": "newConfig",
300 | "type": "tuple",
301 | "internalType": "struct ConfigLib.Config",
302 | "components": [
303 | {
304 | "name": "nonce",
305 | "type": "uint256",
306 | "internalType": "uint256"
307 | },
308 | {
309 | "name": "data",
310 | "type": "bytes",
311 | "internalType": "bytes"
312 | }
313 | ]
314 | },
315 | {
316 | "name": "authorizationProof",
317 | "type": "bytes",
318 | "internalType": "bytes"
319 | }
320 | ],
321 | "outputs": [
322 | {
323 | "name": "",
324 | "type": "bool",
325 | "internalType": "bool"
326 | }
327 | ],
328 | "stateMutability": "view"
329 | },
330 | {
331 | "type": "function",
332 | "name": "implementation",
333 | "inputs": [],
334 | "outputs": [
335 | {
336 | "name": "$",
337 | "type": "address",
338 | "internalType": "address"
339 | }
340 | ],
341 | "stateMutability": "view"
342 | },
343 | {
344 | "type": "function",
345 | "name": "initialize",
346 | "inputs": [
347 | {
348 | "name": "config",
349 | "type": "tuple",
350 | "internalType": "struct ConfigLib.Config",
351 | "components": [
352 | {
353 | "name": "nonce",
354 | "type": "uint256",
355 | "internalType": "uint256"
356 | },
357 | {
358 | "name": "data",
359 | "type": "bytes",
360 | "internalType": "bytes"
361 | }
362 | ]
363 | }
364 | ],
365 | "outputs": [],
366 | "stateMutability": "nonpayable"
367 | },
368 | {
369 | "type": "function",
370 | "name": "isOwnerAddress",
371 | "inputs": [
372 | {
373 | "name": "account",
374 | "type": "address",
375 | "internalType": "address"
376 | }
377 | ],
378 | "outputs": [
379 | {
380 | "name": "",
381 | "type": "bool",
382 | "internalType": "bool"
383 | }
384 | ],
385 | "stateMutability": "view"
386 | },
387 | {
388 | "type": "function",
389 | "name": "isOwnerBytes",
390 | "inputs": [
391 | {
392 | "name": "account",
393 | "type": "bytes",
394 | "internalType": "bytes"
395 | }
396 | ],
397 | "outputs": [
398 | {
399 | "name": "",
400 | "type": "bool",
401 | "internalType": "bool"
402 | }
403 | ],
404 | "stateMutability": "view"
405 | },
406 | {
407 | "type": "function",
408 | "name": "isOwnerPublicKey",
409 | "inputs": [
410 | {
411 | "name": "x",
412 | "type": "bytes32",
413 | "internalType": "bytes32"
414 | },
415 | {
416 | "name": "y",
417 | "type": "bytes32",
418 | "internalType": "bytes32"
419 | }
420 | ],
421 | "outputs": [
422 | {
423 | "name": "",
424 | "type": "bool",
425 | "internalType": "bool"
426 | }
427 | ],
428 | "stateMutability": "view"
429 | },
430 | {
431 | "type": "function",
432 | "name": "isValidSignature",
433 | "inputs": [
434 | {
435 | "name": "hash",
436 | "type": "bytes32",
437 | "internalType": "bytes32"
438 | },
439 | {
440 | "name": "signature",
441 | "type": "bytes",
442 | "internalType": "bytes"
443 | }
444 | ],
445 | "outputs": [
446 | {
447 | "name": "result",
448 | "type": "bytes4",
449 | "internalType": "bytes4"
450 | }
451 | ],
452 | "stateMutability": "view"
453 | },
454 | {
455 | "type": "function",
456 | "name": "masterChainId",
457 | "inputs": [],
458 | "outputs": [
459 | {
460 | "name": "",
461 | "type": "uint256",
462 | "internalType": "uint256"
463 | }
464 | ],
465 | "stateMutability": "view"
466 | },
467 | {
468 | "type": "function",
469 | "name": "ownerAtIndex",
470 | "inputs": [
471 | {
472 | "name": "index",
473 | "type": "uint256",
474 | "internalType": "uint256"
475 | }
476 | ],
477 | "outputs": [
478 | {
479 | "name": "",
480 | "type": "bytes",
481 | "internalType": "bytes"
482 | }
483 | ],
484 | "stateMutability": "view"
485 | },
486 | {
487 | "type": "function",
488 | "name": "proxiableUUID",
489 | "inputs": [],
490 | "outputs": [
491 | {
492 | "name": "",
493 | "type": "bytes32",
494 | "internalType": "bytes32"
495 | }
496 | ],
497 | "stateMutability": "view"
498 | },
499 | {
500 | "type": "function",
501 | "name": "replaySafeHash",
502 | "inputs": [
503 | {
504 | "name": "hash",
505 | "type": "bytes32",
506 | "internalType": "bytes32"
507 | }
508 | ],
509 | "outputs": [
510 | {
511 | "name": "",
512 | "type": "bytes32",
513 | "internalType": "bytes32"
514 | }
515 | ],
516 | "stateMutability": "view"
517 | },
518 | {
519 | "type": "function",
520 | "name": "setConfig",
521 | "inputs": [
522 | {
523 | "name": "newConfig",
524 | "type": "tuple",
525 | "internalType": "struct ConfigLib.Config",
526 | "components": [
527 | {
528 | "name": "nonce",
529 | "type": "uint256",
530 | "internalType": "uint256"
531 | },
532 | {
533 | "name": "data",
534 | "type": "bytes",
535 | "internalType": "bytes"
536 | }
537 | ]
538 | },
539 | {
540 | "name": "authorizeAndValidateProof",
541 | "type": "bytes",
542 | "internalType": "bytes"
543 | }
544 | ],
545 | "outputs": [],
546 | "stateMutability": "nonpayable"
547 | },
548 | {
549 | "type": "function",
550 | "name": "upgradeToAndCall",
551 | "inputs": [
552 | {
553 | "name": "newImplementation",
554 | "type": "address",
555 | "internalType": "address"
556 | },
557 | {
558 | "name": "data",
559 | "type": "bytes",
560 | "internalType": "bytes"
561 | }
562 | ],
563 | "outputs": [],
564 | "stateMutability": "payable"
565 | },
566 | {
567 | "type": "function",
568 | "name": "validateUserOp",
569 | "inputs": [
570 | {
571 | "name": "userOp",
572 | "type": "tuple",
573 | "internalType": "struct UserOperation",
574 | "components": [
575 | {
576 | "name": "sender",
577 | "type": "address",
578 | "internalType": "address"
579 | },
580 | {
581 | "name": "nonce",
582 | "type": "uint256",
583 | "internalType": "uint256"
584 | },
585 | {
586 | "name": "initCode",
587 | "type": "bytes",
588 | "internalType": "bytes"
589 | },
590 | {
591 | "name": "callData",
592 | "type": "bytes",
593 | "internalType": "bytes"
594 | },
595 | {
596 | "name": "callGasLimit",
597 | "type": "uint256",
598 | "internalType": "uint256"
599 | },
600 | {
601 | "name": "verificationGasLimit",
602 | "type": "uint256",
603 | "internalType": "uint256"
604 | },
605 | {
606 | "name": "preVerificationGas",
607 | "type": "uint256",
608 | "internalType": "uint256"
609 | },
610 | {
611 | "name": "maxFeePerGas",
612 | "type": "uint256",
613 | "internalType": "uint256"
614 | },
615 | {
616 | "name": "maxPriorityFeePerGas",
617 | "type": "uint256",
618 | "internalType": "uint256"
619 | },
620 | {
621 | "name": "paymasterAndData",
622 | "type": "bytes",
623 | "internalType": "bytes"
624 | },
625 | {
626 | "name": "signature",
627 | "type": "bytes",
628 | "internalType": "bytes"
629 | }
630 | ]
631 | },
632 | {
633 | "name": "userOpHash",
634 | "type": "bytes32",
635 | "internalType": "bytes32"
636 | },
637 | {
638 | "name": "missingAccountFunds",
639 | "type": "uint256",
640 | "internalType": "uint256"
641 | }
642 | ],
643 | "outputs": [
644 | {
645 | "name": "validationData",
646 | "type": "uint256",
647 | "internalType": "uint256"
648 | }
649 | ],
650 | "stateMutability": "nonpayable"
651 | },
652 | {
653 | "type": "event",
654 | "name": "KeystoreConfigConfirmed",
655 | "inputs": [
656 | {
657 | "name": "configHash",
658 | "type": "bytes32",
659 | "indexed": true,
660 | "internalType": "bytes32"
661 | },
662 | {
663 | "name": "l1BlockTimestamp",
664 | "type": "uint256",
665 | "indexed": true,
666 | "internalType": "uint256"
667 | }
668 | ],
669 | "anonymous": false
670 | },
671 | {
672 | "type": "event",
673 | "name": "KeystoreConfigSet",
674 | "inputs": [
675 | {
676 | "name": "configHash",
677 | "type": "bytes32",
678 | "indexed": true,
679 | "internalType": "bytes32"
680 | }
681 | ],
682 | "anonymous": false
683 | },
684 | {
685 | "type": "event",
686 | "name": "Upgraded",
687 | "inputs": [
688 | {
689 | "name": "implementation",
690 | "type": "address",
691 | "indexed": true,
692 | "internalType": "address"
693 | }
694 | ],
695 | "anonymous": false
696 | },
697 | {
698 | "type": "error",
699 | "name": "BeaconRootDoesNotMatch",
700 | "inputs": [
701 | {
702 | "name": "expected",
703 | "type": "bytes32",
704 | "internalType": "bytes32"
705 | },
706 | {
707 | "name": "actual",
708 | "type": "bytes32",
709 | "internalType": "bytes32"
710 | }
711 | ]
712 | },
713 | {
714 | "type": "error",
715 | "name": "BeaconRootsOracleCallFailed",
716 | "inputs": [
717 | {
718 | "name": "callData",
719 | "type": "bytes",
720 | "internalType": "bytes"
721 | }
722 | ]
723 | },
724 | {
725 | "type": "error",
726 | "name": "ConfirmedConfigOutdated",
727 | "inputs": [
728 | {
729 | "name": "currentConfirmedConfigTimestamp",
730 | "type": "uint256",
731 | "internalType": "uint256"
732 | },
733 | {
734 | "name": "newConfirmedConfigTimestamp",
735 | "type": "uint256",
736 | "internalType": "uint256"
737 | }
738 | ]
739 | },
740 | {
741 | "type": "error",
742 | "name": "ConfirmedConfigTooOld",
743 | "inputs": []
744 | },
745 | {
746 | "type": "error",
747 | "name": "ExecutionBlockHashMerkleProofFailed",
748 | "inputs": []
749 | },
750 | {
751 | "type": "error",
752 | "name": "InitialNonceIsNotZero",
753 | "inputs": []
754 | },
755 | {
756 | "type": "error",
757 | "name": "InvalidConfig",
758 | "inputs": [
759 | {
760 | "name": "configHash",
761 | "type": "bytes32",
762 | "internalType": "bytes32"
763 | },
764 | {
765 | "name": "recomputedConfigHash",
766 | "type": "bytes32",
767 | "internalType": "bytes32"
768 | }
769 | ]
770 | },
771 | {
772 | "type": "error",
773 | "name": "InvalidEthereumAddressOwner",
774 | "inputs": [
775 | {
776 | "name": "owner",
777 | "type": "bytes",
778 | "internalType": "bytes"
779 | }
780 | ]
781 | },
782 | {
783 | "type": "error",
784 | "name": "InvalidL2BlockHeader",
785 | "inputs": [
786 | {
787 | "name": "blockHeaderHash",
788 | "type": "bytes32",
789 | "internalType": "bytes32"
790 | },
791 | {
792 | "name": "blockHash",
793 | "type": "bytes32",
794 | "internalType": "bytes32"
795 | }
796 | ]
797 | },
798 | {
799 | "type": "error",
800 | "name": "InvalidL2OutputRootPreimages",
801 | "inputs": []
802 | },
803 | {
804 | "type": "error",
805 | "name": "InvalidNewKeystoreConfig",
806 | "inputs": []
807 | },
808 | {
809 | "type": "error",
810 | "name": "InvalidNonceKey",
811 | "inputs": [
812 | {
813 | "name": "key",
814 | "type": "uint256",
815 | "internalType": "uint256"
816 | }
817 | ]
818 | },
819 | {
820 | "type": "error",
821 | "name": "InvalidOwnerBytesLength",
822 | "inputs": [
823 | {
824 | "name": "owner",
825 | "type": "bytes",
826 | "internalType": "bytes"
827 | }
828 | ]
829 | },
830 | {
831 | "type": "error",
832 | "name": "InvalidProofType",
833 | "inputs": [
834 | {
835 | "name": "proofType",
836 | "type": "uint8",
837 | "internalType": "uint8"
838 | }
839 | ]
840 | },
841 | {
842 | "type": "error",
843 | "name": "KeystoreAlreadyInitialized",
844 | "inputs": []
845 | },
846 | {
847 | "type": "error",
848 | "name": "L1BlockHashMismatch",
849 | "inputs": [
850 | {
851 | "name": "l1Blockhash",
852 | "type": "bytes32",
853 | "internalType": "bytes32"
854 | },
855 | {
856 | "name": "expectedL1BlockHash",
857 | "type": "bytes32",
858 | "internalType": "bytes32"
859 | }
860 | ]
861 | },
862 | {
863 | "type": "error",
864 | "name": "NonceNotIncrementedByOne",
865 | "inputs": [
866 | {
867 | "name": "currentNonce",
868 | "type": "uint256",
869 | "internalType": "uint256"
870 | },
871 | {
872 | "name": "newNonce",
873 | "type": "uint256",
874 | "internalType": "uint256"
875 | }
876 | ]
877 | },
878 | {
879 | "type": "error",
880 | "name": "NotOnReplicaChain",
881 | "inputs": []
882 | },
883 | {
884 | "type": "error",
885 | "name": "SelectorNotAllowed",
886 | "inputs": [
887 | {
888 | "name": "selector",
889 | "type": "bytes4",
890 | "internalType": "bytes4"
891 | }
892 | ]
893 | },
894 | {
895 | "type": "error",
896 | "name": "Unauthorized",
897 | "inputs": []
898 | },
899 | {
900 | "type": "error",
901 | "name": "UnauthorizedCallContext",
902 | "inputs": []
903 | },
904 | {
905 | "type": "error",
906 | "name": "UnauthorizedCaller",
907 | "inputs": []
908 | },
909 | {
910 | "type": "error",
911 | "name": "UnauthorizedNewKeystoreConfig",
912 | "inputs": []
913 | },
914 | {
915 | "type": "error",
916 | "name": "UpgradeFailed",
917 | "inputs": []
918 | }
919 | ]
920 |
--------------------------------------------------------------------------------
/abis/SmartWalletFactory.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "constructor",
4 | "inputs": [
5 | {
6 | "name": "implementation_",
7 | "type": "address",
8 | "internalType": "address"
9 | }
10 | ],
11 | "stateMutability": "payable"
12 | },
13 | {
14 | "type": "function",
15 | "name": "createAccount",
16 | "inputs": [
17 | {
18 | "name": "configData",
19 | "type": "bytes",
20 | "internalType": "bytes"
21 | },
22 | {
23 | "name": "nonce",
24 | "type": "uint256",
25 | "internalType": "uint256"
26 | }
27 | ],
28 | "outputs": [
29 | {
30 | "name": "account",
31 | "type": "address",
32 | "internalType": "contract CoinbaseSmartWallet"
33 | }
34 | ],
35 | "stateMutability": "payable"
36 | },
37 | {
38 | "type": "function",
39 | "name": "getAddress",
40 | "inputs": [
41 | {
42 | "name": "initialConfigData",
43 | "type": "bytes",
44 | "internalType": "bytes"
45 | },
46 | {
47 | "name": "nonce",
48 | "type": "uint256",
49 | "internalType": "uint256"
50 | }
51 | ],
52 | "outputs": [
53 | {
54 | "name": "",
55 | "type": "address",
56 | "internalType": "address"
57 | }
58 | ],
59 | "stateMutability": "view"
60 | },
61 | {
62 | "type": "function",
63 | "name": "implementation",
64 | "inputs": [],
65 | "outputs": [
66 | {
67 | "name": "",
68 | "type": "address",
69 | "internalType": "address"
70 | }
71 | ],
72 | "stateMutability": "view"
73 | },
74 | {
75 | "type": "function",
76 | "name": "initCodeHash",
77 | "inputs": [],
78 | "outputs": [
79 | {
80 | "name": "",
81 | "type": "bytes32",
82 | "internalType": "bytes32"
83 | }
84 | ],
85 | "stateMutability": "view"
86 | },
87 | {
88 | "type": "error",
89 | "name": "KeyRequired",
90 | "inputs": []
91 | }
92 | ]
93 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/base/keyspace-client/be000f8bb26ca67f0056d07062b7b077f6b21b00/bun.lockb
--------------------------------------------------------------------------------
/docs/pages/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | description: How to use Keyspace to build cross-chain wallets and other applications
4 | ---
5 |
6 | # Getting Started
7 |
8 | Keyspace is a keystore for cross-chain smart wallets that keeps their configuration in sync. It's designed to help wallet vendors build smart wallets that match the cross-chain user experience of externally-owned accounts in Ethereum: one address can be used on all current and future chains. Keyspace can also be used to build new wallet experiences where a user can manage many different accounts that share the same configuration, which makes stealth addresses and other pseudonymous account experiences possible.
9 |
10 | We believe that ERC 4337 smart wallets are the future: passkey signers, paymasters, and batch transactions dramatically improve the experiences builders can provide for their users. But to deliver on that promise, we need users to be confident that assets sent to their Ethereum address are in their control, regardless of what chains those assets are on.
11 |
12 | Keyspace is built by the Base team as open, neutral infrastructure for all chains and wallets to rely on. It's inspired by Vitalik's initial design for a [dedicated minimal rollup for keystores](https://notes.ethereum.org/@vbuterin/minimal_keystore_rollup) and his vision for [cross-L2 interoperability improvements](https://vitalik.eth.limo/general/2024/10/17/futures2.html#6).
13 |
14 |
15 |
16 | * [Keyspace on GitHub](https://github.com/base-org/keyspace)
17 | * [Coinbase Smart Wallet](https://www.smartwallet.dev/) Keyspace integration
18 | * [JavaScript Client](https://github.com/base-org/keyspace-client)
19 | * [Smart Contracts](https://github.com/niran/smart-wallet/tree/keyspace)
20 |
--------------------------------------------------------------------------------
/docs/pages/keystore-basics.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Keystore Basics
3 | description: Learn the fundamental concepts and operations of Keyspace keystores
4 | ---
5 |
6 | # Keystore Basics
7 |
8 | ## Inheriting a Keystore
9 |
10 | In Keyspace, a wallet's cross-chain keystore is typically embedded within the wallet's smart contract. When you inherit from the `Keystore` contract, your wallet gains the ability to sync its configuration across chains.
11 |
12 | Since a `Keystore` needs know how to read its storage across chains, the logic for verifying cross-chain proofs from your wallet's master chain needs to be provided. The `OPStackKeystore` contract shipped with Keyspace provides this logic for OP Stack L2s.
13 |
14 | ## Configuration Hooks
15 |
16 | Of the `Keystore`'s virtual methods that you'll need to implement, the most important are the configuration hooks that are called when the wallet's configuration is changed.
17 |
18 | Here's the core logic for configuration updates:
19 |
20 | ```solidity
21 | // Hook before (to authorize the new Keystore config).
22 | require(
23 | _hookIsConfigAuthorized({config: config, authorizationProof: authorizeAndValidateProof}),
24 | UnauthorizedNewKeystoreConfig()
25 | );
26 |
27 | // Apply the new Keystore config to the internal storage.
28 | bytes32 configHash = applyConfigInternal(config);
29 |
30 | // Hook between (to apply the new Keystore config).
31 | bool triggeredUpgrade = _hookApplyConfig({config: config});
32 |
33 | // Hook after (to validate the new Keystore config).
34 | bool isNewConfigValid = triggeredUpgrade
35 | ? this.hookIsConfigValid({config: config, validationProof: authorizeAndValidateProof})
36 | : hookIsConfigValid({config: config, validationProof: authorizeAndValidateProof});
37 |
38 | require(isNewConfigValid, InvalidNewKeystoreConfig());
39 | ```
40 |
41 | ### _hookIsConfigAuthorized
42 |
43 | `_hookIsConfigAuthorized(ConfigLib.Config calldata config, bytes calldata authorizationProof)`
44 |
45 | This hook is called before the configuration update is applied. It should verify that the caller is authorized to change the configuration. `authorizationProof` is typically a pair of ECDSA signatures for most wallets. Only the first signature is relevant for `_hookIsConfigAuthorized`. (The second signature is used for the `hookIsConfigValid`.)
46 |
47 | If the signature is valid, the hook should return successfully. Otherwise, it should revert.
48 |
49 | ### _hookApplyConfig
50 |
51 | `_hookApplyConfig(ConfigLib.Config calldata config)`
52 |
53 | Once a configuration update is authorized, the `_hookApplyConfig` is called to apply the update to your wallet's internal storage. There are two typical tasks and one optional task that are performed in this hook:
54 |
55 | 1. Check if the implementation of the wallet needs to be upgraded. If the new configuration stored in the keystore has an implementation address that is different from the address stored in the wallet's storage for its proxy to use, the wallet's implementation should be upgraded.
56 | 2. Update the wallet's storage with the new configuration. Typically, you'll just decode `config.data` into your locally defined configuration struct and store it.
57 | 3. (Optional) Store any synthesized data from the new configuration. For example, if your wallet uses a mapping of signers, that mapping cannot be serialized into `config.data` as a bytes array. So, you'll need to iterate through the signers and initialize the mapping in the wallet's storage. To get a fresh mapping with each configuration update, you can store this data in its own mapping keyed by the current configuration hash: a new configuration hash will give you a fresh mapping.
58 |
59 | ### hookIsConfigValid (optional)
60 |
61 | `hookIsConfigValid(ConfigLib.Config calldata config, bytes calldata validationProof)`
62 |
63 | This hook is called after the configuration update is applied. It's optional. If implemented, it may verify that the update is valid. If the update is invalid, the hook should revert.
64 |
65 | A typical implementation will validate a signature of the new configuration hash that is expected to be valid with the new configuration. If the signer of the configuration update is still a valid signer, a separate signature is not needed: just revalidate the same signature that was validated in the `_hookIsConfigAuthorized` using the updated configuration.
66 |
67 | Otherwise, a second signature is needed, and it can be packed into `validationProof` as a pair of signatures. The simplest scenario where a second signature is needed is when a signer is removed from the wallet. This hook would prevent a signer from removing itself without another signature from another signer that proves the new configuration is still usable.
68 |
69 | :::note
70 | This type of validation is new for wallets, and has been introduced to complement Keyspace's state-based configuration management. Most wallets use mutation-based configuration management, where changes are made by functions that apply a specific change to the configuration, like "add signer" or "remove signer." In Keyspace, the wallet's entire configuration is overwritten with each update, which creates more scenarios where the wallet can be misconfigured.
71 |
72 | If you take advantage of this hook, it has implications for your wallet's user experience. In multisig wallet scenarios where the signing threshold is increased in a configuration update, this would force the signers to collect the *new* threshold number of signatures to update the configuration, which is safer than the status quo, but is a meaningfully different user experience.
73 | :::
74 |
75 | When an implementation upgrade has occurred, `hookIsConfigValid` is called via an external call to ensure that the new implementation is executed by the proxy contract for validation.
76 |
--------------------------------------------------------------------------------
/docs/pages/maintaining-wallets.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Maintaining Wallets
3 | description: Learn how to manage wallet upgrades, new chains, and handle hard forks
4 | ---
5 |
6 | # Maintaining Wallets
7 |
8 | Beyond adding and removing signers, cross-chain syncing introduces additional considerations for wallet maintenance.
9 |
10 | ## Performing Wallet Upgrades
11 |
12 | A wallet's configuration is deeply intertwined with the implementation contract that reads that configuration. Upgrading a wallet's implementation alone could break if it could not read the existing configuration, and updating the configuration without upgrading the implementation could break if the new implementation expects a new configuration. Keyspace can help wallet vendors implement atomic upgrades that ensure both the configuration and the implementation are upgraded at the same time.
13 |
14 | To do this, the implementation address for the wallet can be stored in the keystore configuration that is synced across chains. Your wallet's [`_hookApplyConfig`](/keystore-basics#_hookapplynewconfig) is responsible for detecting the new implementation address and performing the upgrade. This new implementation address needs to be deployed on each chain supported by the wallet or the wallet will not be usable on those chains after syncing.
15 |
16 | :::note
17 | If your wallet provides direct access to `upgradeTo` or the equivalent for your proxy contract, the implementation can be overwritten without using Keyspace, and since this change will not be synced, the user will end up with inconsistent behavior across chains. Without care, this could lead to unexpected behavior.
18 | :::
19 |
20 | ## New Chains and Wallet Factories
21 |
22 | Most wallet clients provide an address-centric user experience where the user has the same address on all chains, just like they would with an externally owned account. Smart wallet addresses are forever tied to the factory contract that created them, so there always needs to be a path to start from scratch and end up with the latest wallet configuration on a new chain.
23 |
24 | ### Activating a New Chain
25 |
26 | To activate your wallet on a new chain, it must be freshly initialized by the original factory then synced with the latest configuration from the master chain. The path to accomplish this depends on the wallet's configuration history for the signer that is currently trying to activate the wallet.
27 |
28 | If the signer was in the initial configuration, it's already authorized to pay gas fees on the new chain. It can sync to the latest configuration from the master chain and start signing transactions.
29 |
30 | If the signer was added to the configuration at any point, it will not have permission to pay gas fees on the new chain. The signer is back in the same situation they were in [when they were added to the wallet](/using-new-signers): the wallet vendor or an another existing signer can help them get synced, or they can sync themselves from another chain with assets.
31 |
32 | In either case, it's possible that syncing no longer works because hard forks have occurred on the master chain, the new replica chain, or any chain in between. If that has happened, the wallet can still be configured by replaying every configuration update from the master chain as a setConfig call on the new chain. Assuming the wallet has upgraded its implementation to handle the hard forks, the setConfig calls will also upgrade the implementation on the new chain. A sync can then be performed to bring the wallet up to date and allow user operations to be executed.
33 |
34 | :::note
35 | If cross-chain Merkle proofs have stopped working due to hard forks, another way to restore syncing is via withdrawal and deposit transactions. This process is slow and costly, but is the most resilient method to sync wallets across rollups.
36 | :::
37 |
38 | #### Recovery Guardians
39 |
40 | Recovery guardians affect whether the wallet can be activated on a new chain. Recovery guardians are typically stateful: the recovery is initiated on a blockchain, then can only be processed after a delay that is verified by proving the elapsed time since the recovery was initiated. Since recovery guardians require external state to be verified, they are not guaranteed to always successfully re-execute as setConfig calls on new replica chains indefinitely.
41 |
42 | The most straightforward way to build a recovery guardian with Keyspace is to write it as a periphery contract that is added as a signer to the wallet. When the conditions for the guardian are met, it calls `setConfig` directly on the wallet. `_hookIsConfigAuthorized` can then check `msg.sender` to verify that the guardian's address is authorized in the wallet's configuration.
43 |
44 | This type of configuration change is not as straightforward to re-execute on other chains: instead of just calling `setConfig` with each authorization proof, the user would need to call the recovery guardian itself to initiate the configuration change. Instead of replaying recovery guardian configuration changes, wallets should sync from the master chain to get the latest configuration. Syncing is also required to claim the wallet's address on new chains, and since cross-chain proofs are fragile across hard forks, syncing via withdrawal and deposit transactions becomes even more important for wallets that have used recovery guardians to change their configuration: it's the only way for them to use new chains.
45 |
46 | Until cheap cross-chain state proofs are robust, we recommend that wallet vendors treat recovery guardians as a method of **asset recovery**, not as a method of **wallet recovery**. That is, once the guardian restores access to the wallet's assets, the user should be encouraged to create a new wallet address and move their assets to it.
47 |
48 | ### Adding Support for New Chains
49 |
50 | To support as many chains as possible, wallet factories should aim for permissionless deployment without any admin keys for contract upgrades.
51 |
52 | * Deploy the original, current, and any intermediate wallet implementation contracts on the new chain
53 | * Deploy the wallet factory contract on the new chain
54 |
55 | ## Syncing after Hard Forks
56 |
57 | Wallet vendors must be prepared to review every hard fork on L1 and every rollup they support. When necessary, they should implement the changes required to upgrade the wallet's implementation of cross-chain Merkle proof verification. If the upgrade isn't performed before the fork, syncing will need to rely on withdrawal and deposit transactions.
58 |
59 | Here are the types of hard forks that wallet vendors should be prepared for:
60 |
61 | ### L1 Hard Forks
62 |
63 | * State Root changes (Verkle tree transition)
64 |
65 | ### Master Chain Hard Forks
66 |
67 | * Any change to output roots
68 | * AnchorStateRegistry changes
69 | * DisputeGameFactory changes
70 | * DisputeGame changes
71 |
72 | ### Replica Chain Hard Forks
73 |
74 | * State Root changes (Verkle tree transition)
75 | * L1 state root access
76 | * L1Block.hash changes
77 | * EIP 4788 changes
78 |
--------------------------------------------------------------------------------
/docs/pages/references.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: References
3 | ---
4 |
5 | # References
6 |
7 | * Vitalik's Keystore Vision
8 | * [Dedicated minimal rollup for keystores](https://notes.ethereum.org/@vbuterin/minimal_keystore_rollup), by Vitalik Buterin
9 | * [Deeper dive on cross-L2 reading for wallets and other use cases](https://vitalik.eth.limo/general/2023/06/20/deeperdive.html), by Vitalik Buterin
10 | * [What kind of layer 3s make sense?](https://vitalik.eth.limo/general/2022/09/17/layer_3.html), by Vitalik Buterin
11 | * [The Three Transitions](https://vitalik.eth.limo/general/2023/06/09/three_transitions.html), by Vitalik Buterin
12 | * [Possible futures of the Ethereum protocol, part 2: The Surge](https://vitalik.eth.limo/general/2024/10/17/futures2.html), by Vitalik Buterin
13 | * Other Perspectives on the Keystore Problem
14 | * [Keystore Rollup: Revolutionizing Smart Account Interoperability](https://safe.global/blog/keystore-rollup-smart-account-interoperability), by Lukas Schor & Safe
15 | * [Towards the wallet endgame with Keystore](https://scroll.io/blog/towards-the-wallet-endgame-with-keystore), by Dom, Ye Zhang, & Scroll
16 | * [Keystore Design](https://hackmd.io/@haichen/keystore), by Haichen Shen & Scroll
17 | * [Micro-Rollups for Keystores](https://mirror.xyz/stackrlabs.eth/4kLzcBdvWnvECxOnaGQ8prAqUZEzj4BoDoXqSSXiV6w), by zkcat, Dhruv, & Stackr Labs
18 | * Cross-Chain Messaging
19 | * [Hashi - A principled approach to bridges](https://ethresear.ch/t/hashi-a-principled-approach-to-bridges/14725), by Martin Koeppelmann & Gnosis
20 | * [How can a Safe hold asset on multiple chains?](https://forum.safe.global/t/how-can-a-safe-hold-asset-on-multiple-chains/2242), by Martin Koeppelmann
21 | * [OP Stack Interoperability](https://specs.optimism.io/interop/overview.html), by Optimism
22 | * ERC 4337 Wallets
23 | * [ERC-4337 Simple Whitelist Alt-Mempool](https://hackmd.io/@dancoombs/BJYRz3h8n), by Dan Coombs
24 | * [Account Abstraction in a Multichain Landscape - Part 1: Addresses](https://safe.mirror.xyz/4GcGAOFno-suTCjBewiYH4k4yXPDdIukC5woO5Bjc4w), by Anichohan, Lukas Schor, and Safe
25 |
--------------------------------------------------------------------------------
/docs/pages/releases.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Releases
3 | ---
4 |
5 | # Releases
6 |
7 | ## Contract-Based Alpha (v0.1.0)
8 |
9 | *December 3, 2024*
10 |
11 | Keyspace v0.1.0 is the first smart contract-based implementation of Keyspace. The dedicated keystore rollup has been removed. Keyspace keystores are now stored in the smart wallet contracts themselves. This release supports OP Stack L2s only.
12 |
13 | ## Dedicated Rollup Beta (v0.0.2)
14 |
15 | *June 18, 2024*
16 |
17 | The Dedicated Rollup Beta (v0.0.2) release of Keyspace introduces [`keyspace-client`](https://github.com/base-org/keyspace-client/tree/v0.0.2), an example TypeScript client for Keyspace with an integrated smart wallet, and [`keyspace-recovery-service`](https://github.com/base-org/keyspace-recovery-service/tree/v0.0.2), an RPC service for generating SNARK proofs of the signatures users sign to change their keys. The supported chains have been expanded from Base Sepolia and Optimism Sepolia to include Arbitrum Sepolia, Gnosis Chiado, Polygon Amoy, BSC Testnet, and Avalanche Fuji.
18 |
19 | ## Dedicated Rollup Alpha (v0.0.1)
20 |
21 | *March 29, 2024*
22 |
23 | Initial preview release of Keyspace, including a running Keyspace sequencer and an example Go client for Keyspace's RPC calls.
24 |
--------------------------------------------------------------------------------
/docs/pages/revoking-signers.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Revoking Signers
3 | description: Learn how to safely revoke signers and handle compromised signer scenarios
4 | ---
5 |
6 | # Revoking Signers
7 |
8 | One of the hardest problems for cross-chain wallets is revoking compromised signers. Most cross-chain wallets have no single source of truth for the wallet's configuration, so revoking a compromised signer requires broadcasting the revocation across all active chains. Keyspace introduces the ability to sync configuration changes across chains, which is the first step towards making revoking compromised signers easier. However, there are still two avenues for a compromised signer to take control of a wallet:
9 |
10 | 1. The target replica chain is **actively** used by the wallet, so it has been synced, but using data from 3.5+ days ago due to the master chain's settlement delay.
11 | 2. The target replica chain is **inactive** and has never been synced, so the configuration that contains the compromised signer is still valid on that chain, *no matter how much time has elapsed*. (This is not a Keyspace-specific issue: it's a problem for most cross-chain wallets.)
12 |
13 | Keyspace handles the first scenario by allowing you to replay revocations on active chains. The second scenario is currently infeasible to mitigate, so wallet vendors must understand the implications of revocations on inactive chains.
14 |
15 | ## Replaying Revocations on Active Chains
16 |
17 | When a signer is being removed from a wallet, the wallet client must assist the user in replaying the revocation on all active chains to protect the wallet's assets. This is different from what happens when a new signer is added, where the wallet client only needs to replay the new signer on any replica chains that the new signer wants to send user operations on.
18 |
19 | The easiest way to perform this action will typically be to send a bundle of cross-chain transactions for each chain within one transaction on a single chain. This avoids the need for gas fees to be paid on each chain, and avoids the need for an extra user operation signature for each chain. Instead, payment occurs on the single source chain for the cross-chain transactions.
20 |
21 | ## Compromised Scenarios on Inactive Chains
22 |
23 | Wallet clients will typically not replay revocations on known chains where the wallet has no assets or activity. In addition to those chains, there will always be new chains that launch after a signer is compromised. That means that wallets with revoked signers will always be vulnerable on any chain that has not yet been synced. The attacker will always be able to replay enough setConfig calls to ensure that their key has been added to the wallet, then will be able to diverge from the master chain's configuration at will, including implementation upgrades that can completely change the wallet's behavior.
24 |
25 | ### Missed Chains
26 |
27 | An attacker can use the compromised signer to take control of the wallet on any known chain where the revocation was not replayed.
28 |
29 | ### New Chains
30 |
31 | An attacker can take control of the wallet address on any new chain that is created after the signer was compromised.
32 |
33 | ### Rollups of Missed or New Chains
34 |
35 | An attacker can take control of the wallet address on any rollup of a missed or new chain.
36 |
37 | ## Future Options for Inactive Chains
38 |
39 | Cost-effective syncing depends on cross-chain Merkle proofs, but these can break with each L1 or L2 hard fork. The fallback syncing method is to use withdrawal and deposit transactions, which require multiple transactions, include three transactions on L1 where gas is cost-prohibitive. If we can make the cross-chain Merkle proofs more robust, we can avoid the need for the fallback method.
40 |
41 | The top two paths for addressing this currently seem to be introducing EVM opcodes for direct cross-chain storage reads, or [standardizing cross-chain state proof validation contracts](https://x.com/VitalikButerin/status/1890856500395995599) that are upgraded with each hard fork, just like the withdrawal and deposit bridges are. The [L1SLOAD opcode proposal](https://ethereum-magicians.org/t/rip-7728-l1sload-precompile/20388) would eliminate the risk of L1 hard forks. L2 hard forks would require a similar opcode, either for a widely used master chain for keystores or for a dedicated rollup for keystores. Such a `KEYSTORESLOAD` opcode would eliminate the risk of L2 hard forks.
42 |
43 | Once one of these paths are viable, we expect to change our recommendation to require a recent sync even before setConfig calls can be used, which would limit the window of time that a compromised signer can be used to the eventual consistency window plus the settlement delay of the master chain.
44 |
--------------------------------------------------------------------------------
/docs/pages/roadmap.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Roadmap
3 | ---
4 |
5 | # Roadmap
6 |
7 | ## Cross-Chain Syncing Improvements
8 |
9 | ### Support for More Chains
10 | Keyspace will expand beyond OP Stack L2s to support a wider range of chains for both master and replica functionality. We expect most replica chains to implement EIP-4788 to be able to verify proofs from L1. For alt-L1 replica chains, we plan to add support through multiple trusted oracles using the Hashi project, providing a more secure syncing solution for these chains. For master chains, we'll need to implement an `_extractConfigHashFromMasterChain` function for each rollup stack.
11 |
12 | ### Resilient Fallback Syncing
13 | We will implement syncing via deposits and withdrawals as a resilient fallback method when other syncing methods aren't available or have stopped functioning due to hard forks. Once this has been implemented, wallets should be able to sync on each chain as long as each chain between the master and the replica continues to function.
14 |
15 | ### Standardized Beacon Root Oracle Access
16 | ERC-4337 prohibits cross-contract storage access during the validation phase of user operations. We are working to standardize access to the EIP-4788 beacon root oracle access during ERC-4337 validation across all bundlers. Each slot can only be written to once per day, so it's straightforward for bundlers to defend themselves from the mass invalidation attacks that these restrictions were intended to prevent. This will allow new signers to sync their wallet's configuration to a replica chain without assistance.
17 |
18 | ### L1SLOAD
19 | We currently use either Merkle proofs or withdrawal and deposit transactions to sync wallets across chains. Merkle proofs are more efficient, but are fragile because each hard fork of an L1 or L2 can change the assumptions that the Merkle proof relies on, and would require a contract upgrade with new logic to verify cross-chain state. Withdrawal and deposit transactions are more resilient, but are costly and slow. `L1SLOAD` provides a way for rollups to read state directly from L1, which is fast, cheap, and resilient to L1 and replica chain hard forks. (Master chain hard forks would still be fragile.) It would also lower the gas costs for syncing wallets across chains: each Merkle proof is about 5kb of calldata that direct state reading eliminates.
20 |
21 | ### Standards for Cross-Chain Proofs
22 | Since deposits and withdrawals are costly and slow, we aim to push for standardized contracts for verifying cross-chain proofs that are updated with each hard fork, just like the deposit and withdrawal bridges are. When all rollups offer such contracts, it will always be possible to sync wallets between any two chains without slow and costly deposits and withdrawals. At this point, wallets may be able to **require** syncing for all wallet actions, which makes [revoked signers](/revoking-signers) permanently removed once the eventual consistency period has passed.
23 |
24 |
25 | ## Dedicated Keystore Rollup
26 |
27 | Vitalik's original vision for keystore wallets was based on a *dedicated keystore rollup* where keystores would be stored. Keyspace currently uses general purpose L2s as master chains because it makes keystore wallets easier to deploy today. We believe the endgame for keystore wallets is a dedicated keystore rollup, and we see Keyspace as a stepping stone to that future.
28 |
29 | ### Reduce the Hard Fork Burden
30 | General purpose L2s have frequent hard forks that often break cross-chain syncing. A dedicated keystore rollup would have a much smaller number of hard forks and would prioritize maintaining backwards compatibility with existing wallets, so wallet vendors wouldn't have to push as many upgrades to keep their wallets working.
31 |
32 | ### Speed Up Finalization
33 | Many general purpose L2s have long finality times (3.5+ days) because they are optimistic rollups. A dedicated keystore rollup would be a zero-knowledge rollup, so it would have much faster finality times. We expect that a configuration change made on a zero-knowledge rollup could be finalized on L1 within two minutes, and available on L2s within five minutes. This could reduce the need for replayed setConfig calls in some cases, but would still leave a significant delay for new signers to be usable.
34 |
35 | ### KEYSTORESLOAD
36 | Just like `L1SLOAD` would do for L1 state, `KEYSTORESLOAD` would provide a way for rollups to read state directly from the keystore, which is fast, cheap, and resilient to master chain hard forks. This would make wallets resilient to hard forks of the dedicated keystore rollup and would eliminate more Merkle proofs from calldata when syncing wallets across chains.
37 |
--------------------------------------------------------------------------------
/docs/pages/updating-keystore.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Syncing Your Keystore
3 | description: Learn how Keyspace keeps your wallet's configuration in sync across different chains
4 | ---
5 |
6 | # Updating Your Keystore
7 |
8 | ## Building Your Next Configuration
9 |
10 | Configurations in Keyspace are defined by the `account` address for the keystore, the `data` stored in the keystore, and the `nonce` of the configuration. To make a change to a wallet's configuration, you first need to build the next `data` payload.
11 |
12 | The format of the `data` byte string is defined by the wallet vendor. The most important consideration during an update is that the desired mutation is applied correctly to the configuration without accidentally changing or discarding any other parts of the configuration.
13 |
14 | Once you have the new data, you need the next nonce for the configuration, which is the current nonce plus one. `keyspace-client`'s `buildNextConfig` will fetch the next nonce for you. It also takes the previous configuration data that you applied your mutations to and checks it against the configuration hash stored onchain to make sure you're not applying your changes to an outdated or incorrect configuration.
15 |
16 | To get the configuration hash, call `hashConfig` with the new configuration returned by `buildNextConfig`. That hash needs to be signed to produce the `authorizationProof` needed to make a configuration change, which is consumed by your wallet's hooks (see [Keystore Basics](/keystore-basics)).
17 |
18 | ## Changing the Configuration on a Single Chain
19 |
20 | ```solidity
21 | function setConfig(ConfigLib.Config calldata config, bytes calldata authorizeAndValidateProof)
22 | ```
23 |
24 | `Keystore.setConfig` is the function that writes a configuration change to a chain. It takes the full new configuration struct and the `authorizationProof` produced by signing the configuration hash with the wallet's private key. `setConfig` can be called on the master chain or any replica chain to set the wallet's configuration locally. Since `setConfig` calls can be replayed on other chains, you can develop features that can replay their configuration changes on other chains without any feature-specific syncing work.
25 |
26 | ## Master and Replica Chains
27 |
28 | In Keyspace, a wallet's keystore is built into the wallet contract, and the wallet itself is deployed to multiple chains. The wallet vendor chooses one of these chains to be the **master chain**, the single source of truth for the wallet's configuration. This is typically a general purpose L2 chain with low fees. Every other chain that the wallet is deployed to is a **replica chain**. The distinction between master and replica chains is only relevant for the syncing features described below.
29 |
30 | Keyspace currently ships with support for any OP Stack L2 to be used as a master chain. As of v0.1.0, the replica chains where syncing is supported are OP Stack L2s and any L2 that implements EIP-4788's beacon root oracle. The syncing methods we aim to support are described below.
31 |
32 | ## Syncing to Replica Chains
33 |
34 | Keyspace helps you keep your wallet's configuration in sync across different chains. Replaying `setConfig` calls is the typical way to update a wallet's configuration, but you can optionally sync the latest configuration from the master chain to a replica chain. The primary use case for syncing is expected to be for recovery guardians to execute a recovery on a single chain while syncing the result to any rollup the user wants to use.
35 |
36 | ```solidity
37 | function syncConfig(ConfigLib.Config calldata newConfirmedConfig, bytes calldata keystoreProof)
38 | ```
39 |
40 | `Keystore.syncConfig` syncs the latest configuration from the master chain to a replica chain. It takes the full new configuration struct and a `keystoreProof` with data to prove the configuration hash from the master chain. There are several methods for proving the configuration hash, and the `keystoreProof` will be different depending on the method.
41 |
42 | ### Syncing via Merkle Proofs
43 |
44 | Cross-chain Merkle proofs are the most efficient syncing method, as they only have gas costs on the replica chain the user wants to use their wallet on, which is typically a low-cost L2. They're also the most fragile method: each hard fork of an L1 or L2 can change the assumptions that the Merkle proof relies on, and would require a contract upgrade with new logic to verify cross-chain state.
45 |
46 | `keyspace-client`'s `getMasterKeystoreProofs` retrieves the proofs needed to confirm a configuration change on a replica chain for an OP Stack L2 master chain.
47 |
48 | #### Proving the L1 State Root
49 |
50 | Rollups typically have some way to access the state of the L1 chain. OP Stack rollups have two methods: the `hash` storage slot of the [`L1Block` predeploy](https://specs.optimism.io/protocol/predeploys.html#l1block) and the EIP-4788 beacon root oracle, which can be used to prove the execution state root of a given block. We currently expect rollups to standardize on EIP-4788 as the method to access L1 state because its ring buffer design produces longer-lived proofs, and the beacon chain itself [includes a double-batched accumulator](https://eth2book.info/capella/part3/containers/state/) that makes proofs of any L1 state since the merge much more efficient.
51 |
52 | When using the `L1Block` predeploy, proofs rooted at `L1Block.hash` are only valid for one L1 block time (12 seconds). For longer-lived proofs, we prove the storage slot for `L1Block.hash` and use the `BLOCKHASH` opcode to provide the root for the proof, which lasts for 256 replica chain blocks.
53 |
54 | ##### Proving the L1 State Root on Alt-L1s
55 |
56 | :::warning
57 | Syncing via oracles has important security implications for your wallet. Consider disabling syncing on alt-L1s instead of relying on oracles.
58 | :::
59 |
60 | On alt-L1 chains, there's no trustless way to access the state of the L1 chain. Wallets can either disable syncing on these chains or rely on an oracle to provide the state root. Future releases of Keyspace will use Hashi to require multiple trusted oracles to agree on an L1 block root, then prove the state root from there.
61 |
62 | #### Proving the Master Chain State Root
63 |
64 | Proving the master chain state root typically requires an L1 state proof of the master chain's bridge contract(s). Currently, Keyspace only supports the OP Stack L2s as the master chain via the `OPStackKeystore` contract, which implements `Keystore`'s abstract `_extractConfigHashFromMasterChain` method. Support for other L2s can be implemented by following the same pattern.
65 |
66 | For OP Stack L2s, we prove the `anchors(0)` slot of the `AnchorStateRegistry` contract to prove the latest master chain output root. The state root is part of the preimage of the output root.
67 |
68 | #### Proving the Keystore Configuration Hash
69 |
70 | Once we have the state root for the master chain, we just need to prove storage slots within the wallet contract on that chain. The `Keystore` contract defines its own storage offset for this data, and the configuration hash is stored at that offset.
71 |
72 | ### Syncing via Deposits and Withdrawals
73 |
74 | Deposits and withdrawals are the canonical method for sending messages between chains. That makes them extemely resilient: the whole ecosystem builds on top of withdrawals and deposits with the expectaction that they will succeed for the lifetime of the chains. Rollup teams ensure that their bridge contracts continue to function through each hard fork.
75 |
76 | The downsides of deposits and withdrawals are that they require a transaction to be sent on a separate chain from the one the user is interacting with, and that these transactions have significant costs on L1.
77 |
78 | We expect deposits and withdrawals to mainly be used as method of last resort for syncing your wallet's configuration when the replica chain doesn't have a source of L1 state roots, or when a hard fork has broken the Merkle proof syncing method.
79 |
80 | #### Withdrawing to L1
81 |
82 | Your wallet's configuration can already be deposited to any L3 built on top of the master chain, but not anywhere else. To get your configuration to other chains, the first step is to get it to L1. [The withdrawal flow](https://docs.optimism.io/stack/transactions/withdrawal-flow) requires one transaction on the master chain, then two transactions on L1. It also requires waiting for the challenge period to end before the configuration can be withdrawn to L1.
83 |
84 | #### Depositing to Rollups
85 |
86 | Once your configuration is on L1, you can deposit it to any L2 using their native deposit method. This requires one more L1 transaction and a wait of a few minutes. This method can also be used to sync from an L2 to one of its L3 chains.
87 |
--------------------------------------------------------------------------------
/docs/pages/using-new-signers.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Using New Signers
3 | description: Learn how to add and manage new signers in your Keyspace wallet
4 | ---
5 |
6 | # Using New Signers
7 |
8 | When a new signer is configured, it's written directly to a single chain. But what if the new signer wants to start sending transactions on another chain? You could sync from the master chain if the setConfig call was executed there, but if the new signer was just added, that master chain block won't settle on L1 for 3.5 days (and up to 10 days in the case of optimistic rollup disputes).
9 |
10 | To get make new signers usable immediately, Keyspace supports *replayable setConfig calls*, which allow a `setConfig` call from one chain to be replayed on another chain with the same arguments. Since replica chains can be synced from the master chain, any conflicts between the configurations on a replica chain and the latest master chain configuration will be overwritten by the master chain's configuration.
11 |
12 | ## Replay with the New Signer
13 |
14 | When the existing signer is updating the configuration, the configuration is updated on a single chain. For the new signer to transact on a second chain, the setConfig call needs to be replayed on that chain. The new signer can be authorized to fund the setConfig call with the wallet's assets by processing the setConfig call during the validation phase of the user operation.
15 |
16 | However, if the configuration on a replica chain has diverged from the master chain, it might require a sync before the replay can be performed. A new signer cannot be authorized to fund a sync because syncing violates [ERC-4337's validation rules](https://eips.ethereum.org/EIPS/eip-4337#validation-rules), so someone else needs to initiate the sync. (There are workarounds for ERC-4337 validation restrictions, but we don't expect them to be worthwhile for handling rare divergence scenarios.)
17 |
18 | ## Set, Replay, and Sync via Wallet Vendor
19 |
20 | The simplest scenario is when the wallet vendor takes care of setting the new configuration on a chain, replaying it on the desired chains, and syncing divergent replicas when necessary. The wallet vendor uses their own assets to pay for these actions.
21 |
22 | A straightforward implementation of this method would have the wallet client send a request to the wallet vendor's backend to initiate the set, replay, and sync operations while the client's desired transaction waits for the wallet vendor's configuration calls to be confirmed in a block. To skip this wait, the wallet vendor can also act as the bundler for their users when a new signer is added. The set, replay, and sync calls would then be called right before the user's user operation within the same transaction.
23 |
24 | ## Set, Replay, and Sync with an Existing Signer
25 |
26 | The existing signer can take responsibility for setting the new configuration on the chain the new signer intends to transact on, but the complexity of this operation depends on where the wallet's funds are located.
27 |
28 | 1. The simplest scenario is when the wallet has funds on the chain the new signer intends to transact on. In this case, the existing signer can simply call `setConfig` on that chain.
29 | 2. If the wallet's funds are location on a different chain, the existing signer needs to perform a cross-chain transaction to set the new configuration on the desired chain.
30 | 3. If the new signer wants to transact on a replica chain that has diverged from the canonical configuration history, the existing signer needs to execute a sync and one or more setConfig calls.
31 | 1. If the master chain's configuration is too old to resolve the conflict, the existing signer can execute all the setConfig calls up to the desired configuration on the master chain, then sync the replica chain after the setConfig calls have been confirmed on the master chain.
32 | 2. If the master chain's configuration is recent enough to resolve the conflict, the existing signer can execute a sync and one or more setConfig calls on the replica chain to reach the desired configuration.
33 | 3. In the former case, if the wallet has no assets on the master chain, the existing signer can only set the new configuration via a cross-chain transaction from a replica chain with assets.
34 |
--------------------------------------------------------------------------------
/docs/pages/web-services.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Web Services
3 | ---
4 |
5 | # Web Services
6 |
7 | Smart wallet vendors typically run web services to support their wallet clients. These services allow the client to pull up all wallets associated with the present signing key, store pending transactions for multisig wallets, and more.
8 |
9 | For Keyspace-integrated wallets, wallet vendors can expect to run two support services: an indexer and a recovery proof service.
10 |
11 | ## Signing Key Indexer
12 |
13 | Anyone can run a Keyspace node to have a local copy of the Keyspace database that can be queried directly or via the `mksr_get` JSON RPC call exposed by the node. However, we expect many wallet vendors to want to update their own databases when a user changes their wallet configuration. This helps provide APIs to connect the signing key present in the application with wallets that the key is authorized to access.
14 |
15 | ## Recovery Proof Service
16 |
17 | Keyspace is designed to store the configuration for any kind of wallet regardless of its authentication logic. But that means that wallet vendors must be prepared to help their users generate recovery proofs to change keys using the logic chosen by the vendor. Keyspace will provide an example recovery proof service that generates proofs for the example circuits, but wallet vendors are encouraged to run and customize their own versions of the service.
18 |
19 | ## Multisig Transaction Service
20 |
21 | Smart wallets that authorize transactions with multiple signatures typically run a service to collect signatures from signers until the threshold has been reached, like [Safe Transaction Service](https://docs.safe.global/core-api/api-safe-transaction-service) does. Beyond supporting typical transactions, these services will need to be extended to support Keyspace recovery transactions.
22 |
--------------------------------------------------------------------------------
/dprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript": {
3 | },
4 | "json": {
5 | },
6 | "excludes": [
7 | "**/node_modules",
8 | "**/*-lock.json"
9 | ],
10 | "plugins": [
11 | "https://plugins.dprint.dev/typescript-0.88.10.wasm",
12 | "https://plugins.dprint.dev/json-0.19.1.wasm"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/keystore.bytecode:
--------------------------------------------------------------------------------
1 | 0x608060405234801561001057600080fd5b50600436106100a95760003560e01c806372629ccb1161007157806372629ccb1461012f578063772a00011461014f578063955a33ae1461016a578063e49d092e1461018d578063e7bcd01914610196578063f70af2e3146101a957600080fd5b80630e1d9596146100ae57806327955120146100cb578063298c9005146100de578063340eec2c146100e75780635c0cba33146100fc575b600080fd5b6100b862014a3481565b6040519081526020015b60405180910390f35b6100b86100d9366004611e27565b6101bc565b6100b860015481565b6100fa6100f5366004611e53565b6101fa565b005b610117734c8ba32a5dac2a720bb35cedb51d6b067d10420581565b6040516001600160a01b0390911681526020016100c2565b6100b861013d366004611e8e565b60026020526000908152604090205481565b610117738346284b016a22d23eba31966cffc05b617dc32a81565b61017d610178366004611ef2565b6102f8565b60405190151581526020016100c2565b6100b860005481565b6100fa6101a4366004611faf565b6103a2565b6100fa6101b73660046120b3565b610450565b600360205282600052604060002060205281600052604060002081815481106101e457600080fd5b9060005260206000200160009250925050505481565b600080610238734c8ba32a5dac2a720bb35cedb51d6b067d104205738346284b016a22d23eba31966cffc05b617dc32a6102338661242c565b610613565b60015491935091508181811015610270576040516354ebcdcb60e01b8152600481019290925260248201526044015b60405180910390fd5b50506102b06040518060400160405280602081526020017f4e657720626c6f636b206e756d626572206973206869676820656e6f756768208152506106bd565b6000829055600181905560408051838152602081018390527f3aa10f5a3ea27176228d71504642d1678800d2727bd735057ee9d53c24e1c699910160405180910390a1505050565b60004662014a3403610382576040516301e6472560e01b8152600481018690528490738346284b016a22d23eba31966cffc05b617dc32a906301e6472590602401602060405180830381865afa158015610356573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061037a919061253d565b14905061039a565b6000610392866000548686610703565b871493505050505b949350505050565b60008060006103b58c6000548d8d610703565b9250925092506103cb8c828b8b8b8b8b8b6107b7565b82546000036103ea578254600181018455600084815260209020018290555b8254600181018455600084815260209020018890556040517fd4c1702d122e3b3301ee8550fab8e1127efbf952f227bb5bc1a535b50c5cf1939061043a908e908b90918252602082015260400190565b60405180910390a1505050505050505050505050565b60006104606000548d8d8d610a49565b60008d81526002602090815260408083205460038352818420818552909252822080549394508f939192909182908b90811061049e5761049e612556565b906000526020600020015490508481141585906104d1576040516303607ae760e11b815260040161026791815260200190565b506104dc818a610ab3565b506104ed6040890160208a0161256c565b6001600160601b031661050660408e0160208f0161256c565b6001600160601b03161461052060408e0160208f0161256c565b61053060408b0160208c0161256c565b9091610562576040516302df99af60e31b81526001600160601b03928316600482015291166024820152604401610267565b505061057483858e8e8e8c8c8c6107b7565b61057f6001836125ab565b6000848152600260209081526040808320849055600382528083208484528252808320805460018082018355828652948490209081018a905581549485018255939093018f905580518781529182018f90529294509092507fd4c1702d122e3b3301ee8550fab8e1127efbf952f227bb5bc1a535b50c5cf193910160405180910390a1505050505050505050505050505050565b60008060006106258460000151610b3e565b905061063984602001518260000151610c2c565b600061067782602001518887604001517fa6eef7e35abe7026729641147f7915573c7e97b47efa546f5f6e3230263bcb4960001b8960600151610d1b565b90506106918560a001518660c001518760e0015184610d41565b60006106a68660a00151888860800151610da7565b60409093015192945091925050505b935093915050565b610700816040516024016106d191906125e2565b60408051601f198184030181529190526020810180516001600160e01b031663104c13eb60e21b179052610e6a565b50565b6000848152600260209081526040808320546003835281842081855290925282209190819061073487898888610a49565b92508261073f578792505b82915060005b84548110156107ab578385828154811061076157610761612556565b9060005260206000200154036107a3578454859061078190600190612615565b8154811061079157610791612556565b906000526020600020015492506107ab565b600101610745565b50509450945094915050565b6107c18787610ab3565b6107cb8585610ab3565b6107db604087016020880161256c565b6107e6906001612628565b6001600160601b03166107ff604086016020870161256c565b6001600160601b031614610819604088016020890161256c565b610829604087016020880161256c565b909161085b576040516306427aeb60e01b81526001600160601b03928316600482015291166024820152604401610267565b505060408051608081018252600080825260208201819052918101829052606081019190915282156108b65760008061089685870187612647565b915091506108a382610b3e565b92506108b3818460000151610c2c565b50505b6108c360208801886126ae565b6001600160a01b031663922516b58a6108df60408b018b6126d7565b8a866108eb89806126d7565b6040518863ffffffff1660e01b815260040161090d9796959493929190612746565b6020604051808303816000875af115801561092c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061095091906127ae565b61096d57604051634f1f214760e01b815260040160405180910390fd5b600061097c60208401846126d7565b90501115610a3e5761099160208601866126ae565b6001600160a01b031663922516b58a6109ad60408901896126d7565b8a866109bc60208a018a6126d7565b6040518863ffffffff1660e01b81526004016109de9796959493929190612746565b6020604051808303816000875af11580156109fd573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a2191906127ae565b610a3e57604051637d5ba07f60e01b815260040160405180910390fd5b505050505050505050565b60408051602081018590526000818301819052825180830384018152606083019093529190610aa9908790610a829084906080016127d0565b60408051601f198184030181529190528051602090910120610aa486886127ec565b610e73565b9695505050505050565b6000610ac260208301836126ae565b610ad2604084016020850161256c565b610adf60408501856126d7565b604051602001610af294939291906127f9565b60405160208183030381529060405280519060200120905082811483829091610b37576040516346b2589360e11b815260048101929092526024820152604401610267565b5050505050565b6040805160808082018352600080835260208084018290528385018290526060808501839052855193840186528284528382018390528386018390528301829052845180860186528281528101829052845180860190955285518552858101908501529192909190610bb490610ef6565b610ef6565b9050610bd981600381518110610bcc57610bcc612556565b6020026020010151611001565b60208301528051610bf79082906008908110610bcc57610bcc612556565b60408301528051610c15908290600b908110610bcc57610bcc612556565b606083015250825160209093019290922082525090565b600082516001811115610c4157610c4161283b565b03610c8e5760405162461bcd60e51b815260206004820152601d60248201527f50726f6f664c69623a204e4f545f494d504c454d454e5445445f5945540000006044820152606401610267565b600182516001811115610ca357610ca361283b565b03610cd35760008260200151806020019051810190610cc29190612922565b9050610cce818361104e565b505050565b60405162461bcd60e51b815260206004820152601c60248201527f50726f6f664c69623a20494e56414c49445f50524f4f465f54595045000000006044820152606401610267565b600080610d29878787610da7565b9050610d36818585610e73565b979650505050505050565b604080516000602082018190529181018690526060810185905260808101849052819060a001604051602081830303815290604052805190602001209050828114610d9f5760405163f84b89b160e01b815260040160405180910390fd5b505050505050565b6040516bffffffffffffffffffffffff19606084901b1660208201526000908190603401604051602081830303815290604052805190602001209050610e5f610e4d610baf610e208885604051602001610e0391815260200190565b604051602081830303815290604052610e1b896110fb565b6111e2565b60408051808201825260008082526020918201528151808301909252825182529182019181019190915290565b600281518110610bcc57610bcc612556565b9150505b9392505050565b61070081611784565b60008083604051602001610e8991815260200190565b6040516020818303038152906040528051906020012090506000610ed5610e208784604051602001610ebd91815260200190565b604051602081830303815290604052610e1b886110fb565b8051909150600003610eed575060009150610e639050565b610aa981611001565b6060610f01826117a5565b610f0a57600080fd5b6000610f15836117e0565b90506000816001600160401b03811115610f3157610f316121ee565b604051908082528060200260200182016040528015610f7657816020015b6040805180820190915260008082526020820152815260200190600190039081610f4f5790505b5090506000610f888560200151611865565b8560200151610f9791906125ab565b90506000805b84811015610ff657610fae836118e0565b9150604051806040016040528083815260200184815250848281518110610fd757610fd7612556565b6020908102919091010152610fec82846125ab565b9250600101610f9d565b509195945050505050565b80516000901580159061101657508151602110155b61101f57600080fd5b60008061102b84611984565b81519193509150602082101561039a5760208290036101000a9004949350505050565b600061105d8360000151610b3e565b604081015181519192508040918281811461109c5760405163b50f96a160e01b8152600481019390935260248301919091526044820152606401610267565b50505060006110c583602001516015602160991b018760200151600260001b8960400151610d1b565b905080848082146110f257604051635439166b60e11b815260048101929092526024820152604401610267565b50505050505050565b6060600082516001600160401b03811115611118576111186121ee565b60405190808252806020026020018201604052801561115d57816020015b60408051808201909152600080825260208201528152602001906001900390816111365790505b50905060005b83518110156111db576111b684828151811061118157611181612556565b602002602001015160408051808201825260008082526020918201528151808301909252825182529182019181019190915290565b8282815181106111c8576111c8612556565b6020908102919091010152600101611163565b5092915050565b606060006111f18460006119cb565b90506000806060611215604051806040016040528060008152602001600081525090565b8651600003611266577f56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421891461124a57600080fd5b50506040805160008152602081019091529350610e6392505050565b60005b875181101561177757801580156112a857506112a488828151811061129057611290612556565b602002602001015160208101519051902090565b8a14155b156112ea5760405162461bcd60e51b81526020600482015260126024820152710e4dedee840d0c2e6d040dad2e6dac2e8c6d60731b6044820152606401610267565b801580159061131a575061131688828151811061130957611309612556565b6020026020010151611b70565b8414155b1561135c5760405162461bcd60e51b81526020600482015260126024820152710dcdec8ca40d0c2e6d040dad2e6dac2e8c6d60731b6044820152606401610267565b61137e88828151811061137157611371612556565b6020026020010151610ef6565b9250825160020361156d57600060606113b86113b3866000815181106113a6576113a6612556565b6020026020010151611bc7565b611c44565b909250905060006113ca888a84611cdf565b90506113d681896125ab565b975081518110156114395760018b516113ef9190612615565b8410156113fb57600080fd5b60005b6040519080825280601f01601f191660200182016040528015611428576020820181803683370190505b509950505050505050505050610e63565b82156114e25760018b5161144d9190612615565b84101561149c5760405162461bcd60e51b815260206004820152601b60248201527f6c656166206e6f6465206e6f74206c61737420696e2070726f6f6600000000006044820152606401610267565b88518810156114ac5760006113fe565b856001815181106114bf576114bf612556565b602002602001015194506114d285611bc7565b9950505050505050505050610e63565b60018b516114f09190612615565b84036114fb57600080fd5b61151e8660018151811061151157611511612556565b60200260200101516117a5565b61154c576115458660018151811061153857611538612556565b6020026020010151611d6a565b9650611565565b6115628660018151811061129057611290612556565b96505b50505061176f565b825160110361176f57855185146116f057600086868151811061159257611592612556565b016020015160f81c90506115a76001876125ab565b955060108160ff16106115f05760405162461bcd60e51b815260206004820152601160248201527070617468206e6f742061206e6962626c6560781b6044820152606401610267565b611615848260ff168151811061160857611608612556565b6020026020010151611d82565b1561169457600189516116289190612615565b82146116765760405162461bcd60e51b815260206004820152601c60248201527f656d707479206e6f6465206e6f74206c61737420696e2070726f6f66000000006044820152606401610267565b50506040805160008152602081019091529550610e63945050505050565b6116ac848260ff168151811061151157611511612556565b6116cf576116c8848260ff168151811061153857611538612556565b94506116ea565b6116e7848260ff168151811061129057611290612556565b94505b5061176f565b600188516116fe9190612615565b811461174c5760405162461bcd60e51b815260206004820152601d60248201527f6272616e6368206e6f6465206e6f74206c61737420696e2070726f6f660000006044820152606401610267565b611762836010815181106113a6576113a6612556565b9650505050505050610e63565b600101611269565b5050505050509392505050565b60006a636f6e736f6c652e6c6f679050600080835160208501845afa505050565b805160009081036117b857506000919050565b6020820151805160001a9060c08210156117d6575060009392505050565b5060019392505050565b805160009081036117f357506000919050565b6000806118038460200151611865565b846020015161181291906125ab565b905060008460000151856020015161182a91906125ab565b90505b8082101561185c5761183e826118e0565b61184890836125ab565b915082611854816129e7565b93505061182d565b50909392505050565b8051600090811a608081101561187e5750600092915050565b60b8811080611899575060c08110801590611899575060f881105b156118a75750600192915050565b60c08110156118d4576118bc600160b8612a00565b6118c99060ff1682612615565b610e639060016125ab565b6118bc600160f8612a00565b80516000908190811a60808110156118fb57600191506111db565b60b88110156119215761190f608082612615565b61191a9060016125ab565b91506111db565b60c081101561194e5760b78103600185019450806020036101000a855104600182018101935050506111db565b60f88110156119625761190f60c082612615565b60019390930151602084900360f7016101000a900490920160f5190192915050565b60008060006119968460200151611865565b905060008185602001516119aa91906125ab565b905060008286600001516119be9190612615565b9196919550909350505050565b606060008351116119db57600080fd5b6000835160026119eb9190612a19565b9050808311156119fa57600080fd5b611a048382612615565b9050806001600160401b03811115611a1e57611a1e6121ee565b6040519080825280601f01601f191660200182016040528015611a48576020820181803683370190505b5091506000835b611a5983866125ab565b811015611b5757611a6b600282612a46565b600003611ad757600486611a80600284612a5a565b81518110611a9057611a90612556565b602001015160f81c60f81b60f81c60ff16901c600f1660f81b848381518110611abb57611abb612556565b60200101906001600160f81b031916908160001a905350611b38565b600086611ae5600284612a5a565b81518110611af557611af5612556565b602001015160f81c60f81b60f81c60ff16901c600f1660f81b848381518110611b2057611b20612556565b60200101906001600160f81b031916908160001a9053505b611b436001836125ab565b9150611b506001826125ab565b9050611a4f565b5082518114611b6857611b68612a6e565b505092915050565b6000602082600001511015611b8f576020820151825190205b92915050565b602082015182519020604051602001611baa91815260200190565b604051602081830303815290604052805190602001209050919050565b8051606090611bd557600080fd5b600080611be184611984565b915091506000816001600160401b03811115611bff57611bff6121ee565b6040519080825280601f01601f191660200182016040528015611c29576020820181803683370190505b50905060208101611c3b848285611da5565b50949350505050565b600060606000835111611c5657600080fd5b6000600484600081518110611c6d57611c6d612556565b60209101015160f81c901c600f1690506000818103611c925750600092506002611cc9565b81600103611ca65750600092506001611cc9565b81600203611cba5750600192506002611cc9565b816003036100a9575060019250825b83611cd486836119cb565b935093505050915091565b6000805b8351611cef86836125ab565b108015611cfc5750825181105b1561039a57828181518110611d1357611d13612556565b01602001516001600160f81b03191684611d2d87846125ab565b81518110611d3d57611d3d612556565b01602001516001600160f81b03191614611d58579050610e63565b80611d62816129e7565b915050611ce3565b6000806000611d7884611984565b9020949350505050565b8051600090600114611d9657506000919050565b50602001515160001a60801490565b80600003611db257505050565b60208110611dea5782518252611dc96020846125ab565b9250611dd66020836125ab565b9150611de3602082612615565b9050611db2565b8015610cce5760006001611dff836020612615565b611e0b90610100612b63565b611e159190612615565b84518451821691191617835250505050565b600080600060608486031215611e3c57600080fd5b505081359360208301359350604090920135919050565b600060208284031215611e6557600080fd5b81356001600160401b03811115611e7b57600080fd5b82016101008185031215610e6357600080fd5b600060208284031215611ea057600080fd5b5035919050565b60008083601f840112611eb957600080fd5b5081356001600160401b03811115611ed057600080fd5b6020830191508360208260051b8501011115611eeb57600080fd5b9250929050565b60008060008060608587031215611f0857600080fd5b843593506020850135925060408501356001600160401b03811115611f2c57600080fd5b611f3887828801611ea7565b95989497509550505050565b600060608284031215611f5657600080fd5b50919050565b60008083601f840112611f6e57600080fd5b5081356001600160401b03811115611f8557600080fd5b602083019150836020828501011115611eeb57600080fd5b600060408284031215611f5657600080fd5b600080600080600080600080600060e08a8c031215611fcd57600080fd5b8935985060208a01356001600160401b03811115611fea57600080fd5b611ff68c828d01611ea7565b90995097505060408a01356001600160401b0381111561201557600080fd5b6120218c828d01611f44565b96505060608a0135945060808a01356001600160401b0381111561204457600080fd5b6120508c828d01611f44565b94505060a08a01356001600160401b0381111561206c57600080fd5b6120788c828d01611f5c565b90945092505060c08a01356001600160401b0381111561209757600080fd5b6120a38c828d01611f9d565b9150509295985092959850929598565b60008060008060008060008060008060006101208c8e0312156120d557600080fd5b8b359a5060208c01356001600160401b038111156120f257600080fd5b6120fe8e828f01611ea7565b909b5099505060408c01356001600160401b0381111561211d57600080fd5b6121298e828f01611f44565b98505060608c0135965060808c01356001600160401b0381111561214c57600080fd5b6121588e828f01611f44565b96505060a08c0135945060c08c01356001600160401b0381111561217b57600080fd5b6121878e828f01611f44565b94505060e08c01356001600160401b038111156121a357600080fd5b6121af8e828f01611f5c565b9094509250506101008c01356001600160401b038111156121cf57600080fd5b6121db8e828f01611f9d565b9150509295989b509295989b9093969950565b634e487b7160e01b600052604160045260246000fd5b60405161010081016001600160401b0381118282101715612227576122276121ee565b60405290565b604051606081016001600160401b0381118282101715612227576122276121ee565b604051601f8201601f191681016001600160401b0381118282101715612277576122776121ee565b604052919050565b60006001600160401b03821115612298576122986121ee565b50601f01601f191660200190565b600082601f8301126122b757600080fd5b81356122ca6122c58261227f565b61224f565b8181528460208386010111156122df57600080fd5b816020850160208301376000918101602001919091529392505050565b60006040828403121561230e57600080fd5b604080519081016001600160401b0381118282101715612330576123306121ee565b60405290508082356002811061234557600080fd5b815260208301356001600160401b0381111561236057600080fd5b61236c858286016122a6565b6020830152505092915050565b60006001600160401b03821115612392576123926121ee565b5060051b60200190565b60006123aa6122c584612379565b838152905060208101600584901b8301858111156123c757600080fd5b835b818110156124025780356001600160401b038111156123e757600080fd5b6123f3888288016122a6565b845250602092830192016123c9565b5050509392505050565b600082601f83011261241d57600080fd5b610e638383356020850161239c565b6000610100823603121561243f57600080fd5b612447612204565b82356001600160401b0381111561245d57600080fd5b612469368286016122a6565b82525060208301356001600160401b0381111561248557600080fd5b612491368286016122fc565b60208301525060408301356001600160401b038111156124b057600080fd5b6124bc3682860161240c565b60408301525060608301356001600160401b038111156124db57600080fd5b6124e73682860161240c565b60608301525060808301356001600160401b0381111561250657600080fd5b6125123682860161240c565b60808301525060a0838101359082015260c0808401359082015260e092830135928101929092525090565b60006020828403121561254f57600080fd5b5051919050565b634e487b7160e01b600052603260045260246000fd5b60006020828403121561257e57600080fd5b81356001600160601b0381168114610e6357600080fd5b634e487b7160e01b600052601160045260246000fd5b80820180821115611b8957611b89612595565b60005b838110156125d95781810151838201526020016125c1565b50506000910152565b60208152600082518060208401526126018160408501602087016125be565b601f01601f19169190910160400192915050565b81810381811115611b8957611b89612595565b6001600160601b038181168382160190811115611b8957611b89612595565b6000806040838503121561265a57600080fd5b82356001600160401b0381111561267057600080fd5b61267c858286016122a6565b92505060208301356001600160401b0381111561269857600080fd5b6126a4858286016122fc565b9150509250929050565b6000602082840312156126c057600080fd5b81356001600160a01b0381168114610e6357600080fd5b6000808335601e198436030181126126ee57600080fd5b8301803591506001600160401b0382111561270857600080fd5b602001915036819003821315611eeb57600080fd5b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b878152610100602082015260006127626101008301888a61271d565b8660408401528551606084015260208601516080840152604086015160a0840152606086015160c084015282810360e08401526127a081858761271d565b9a9950505050505050505050565b6000602082840312156127c057600080fd5b81518015158114610e6357600080fd5b600082516127e28184602087016125be565b9190910192915050565b6000610e6336848461239c565b606085901b6bffffffffffffffffffffffff1916815260a084901b6001600160a01b031916601482015281836020830137600091016020019081529392505050565b634e487b7160e01b600052602160045260246000fd5b600082601f83011261286257600080fd5b81516128706122c58261227f565b81815284602083860101111561288557600080fd5b61039a8260208301602087016125be565b600082601f8301126128a757600080fd5b81516128b56122c582612379565b8082825260208201915060208360051b8601019250858311156128d757600080fd5b602085015b838110156129185780516001600160401b038111156128fa57600080fd5b612909886020838a0101612851565b845250602092830192016128dc565b5095945050505050565b60006020828403121561293457600080fd5b81516001600160401b0381111561294a57600080fd5b82016060818503121561295c57600080fd5b61296461222d565b81516001600160401b0381111561297a57600080fd5b61298686828501612851565b82525060208201516001600160401b038111156129a257600080fd5b6129ae86828501612896565b60208301525060408201516001600160401b038111156129cd57600080fd5b6129d986828501612896565b604083015250949350505050565b6000600182016129f9576129f9612595565b5060010190565b60ff8281168282160390811115611b8957611b89612595565b8082028115828204841417611b8957611b89612595565b634e487b7160e01b600052601260045260246000fd5b600082612a5557612a55612a30565b500690565b600082612a6957612a69612a30565b500490565b634e487b7160e01b600052600160045260246000fd5b6001815b60018411156106b557808504811115612aa357612aa3612595565b6001841615612ab157908102905b60019390931c928002612a88565b600082612ace57506001611b89565b81612adb57506000611b89565b8160018114612af15760028114612afb57612b17565b6001915050611b89565b60ff841115612b0c57612b0c612595565b50506001821b611b89565b5060208310610133831016604e8410600b8410161715612b3a575081810a611b89565b612b476000198484612a84565b8060001904821115612b5b57612b5b612595565b029392505050565b6000610e638383612abf56fea2646970667358221220f6d474ad590fea863ac30e0ec70d9ab4459a08cfa062ff04bd6468e26a086b6864736f6c634300081b0033
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keyspace",
3 | "version": "0.1.0",
4 | "type": "module",
5 | "scripts": {
6 | "build": "tsc && tsc-alias",
7 | "docs:dev": "vocs dev",
8 | "docs:build": "vocs build",
9 | "docs:preview": "vocs preview"
10 | },
11 | "devDependencies": {
12 | "@types/argparse": "^2.0.16",
13 | "@wagmi/cli": "^2.1.0",
14 | "bun-types": "latest",
15 | "tsc-alias": "^1.8.10"
16 | },
17 | "peerDependencies": {
18 | "typescript": "^5.0.0"
19 | },
20 | "dependencies": {
21 | "@noble/curves": "^1.4.0",
22 | "@zk-kit/poseidon-cipher": "^0.3.0",
23 | "argparse": "^2.0.1",
24 | "dprint": "^0.45.0",
25 | "ecdsa-secp256r1": "^1.3.3",
26 | "maci-crypto": "^1.2.0",
27 | "permissionless": "^0.0.34",
28 | "viem": "^2.21.40",
29 | "vocs": "^1.0.0-alpha.52"
30 | },
31 | "files": [
32 | "dist/**/*"
33 | ],
34 | "exports": {
35 | ".": "./dist/index.js",
36 | "./*": "./dist/*.js"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/scripts/change-owner.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentParser } from "argparse";
2 |
3 | import { defaultToEnv } from "./lib/argparse";
4 | import { CoinbaseSmartWalletConfigData, decodeConfigData, encodeConfigData, getConfigDataWithAddedOwner, getConfigDataWithRemovedOwner } from "../src/wallets/base-wallet/config";
5 | import * as callsSecp256k1 from "../src/wallets/base-wallet/signers/secp256k1/calls";
6 | import * as callsWebAuthn from "../src/wallets/base-wallet/signers/webauthn/calls";
7 | import { buildNextConfig, buildSetConfigCalldata } from "../src/config";
8 | import { masterClient } from "./lib/client";
9 | import { Call } from "../src/wallets/base-wallet/user-op";
10 | const P256 = require("ecdsa-secp256r1");
11 |
12 |
13 | async function main() {
14 | const parser = new ArgumentParser({
15 | description: "Change owner of a keystore wallet",
16 | });
17 |
18 | parser.add_argument("--account", {
19 | help: "The account of the keystore wallet to send from",
20 | required: true,
21 | });
22 | parser.add_argument("--owner-index", {
23 | help: "The index of the owner",
24 | default: 0,
25 | });
26 | parser.add_argument("--initial-config-data", {
27 | help: "The initial config data needed to deploy the wallet as a hex string",
28 | });
29 | parser.add_argument("--private-key", {
30 | help: "The current private key of the owner",
31 | ...defaultToEnv("PRIVATE_KEY"),
32 | });
33 | parser.add_argument("--config-data", {
34 | help: "The current config data for the keystore wallet as a hex string",
35 | required: true,
36 | });
37 | parser.add_argument("--owner-bytes", {
38 | help: "The owner bytes to change in the keystore wallet",
39 | required: true,
40 | });
41 | parser.add_argument("--signature-type", {
42 | help: "The type of signature for the private key",
43 | default: "secp256k1",
44 | });
45 | parser.add_argument("--remove", {
46 | help: "Remove the owner from the keystore wallet",
47 | action: "store_true",
48 | });
49 |
50 | const args = parser.parse_args();
51 |
52 | let privateKey: any;
53 | let callsModule: any;
54 | if (args.signature_type === "secp256k1") {
55 | console.log("Using secp256k1 private key...");
56 | privateKey = args.private_key;
57 | callsModule = callsSecp256k1;
58 | } else if (args.signature_type === "webauthn") {
59 | console.log("Using WebAuthn private key...");
60 | privateKey = P256.fromJWK(JSON.parse(args.private_key));
61 | callsModule = callsWebAuthn;
62 | } else {
63 | console.error("Invalid signature type");
64 | }
65 |
66 | const currentConfigData = decodeConfigData(args.config_data);
67 | let newConfigData: CoinbaseSmartWalletConfigData;
68 | if (args.remove) {
69 | console.log("Removing owner from keystore wallet...");
70 | newConfigData = getConfigDataWithRemovedOwner(currentConfigData, args.owner_bytes);
71 | } else {
72 | console.log("Adding owner to keystore wallet...");
73 | newConfigData = getConfigDataWithAddedOwner(currentConfigData, args.owner_bytes);
74 | }
75 |
76 | const encodedNewConfigData = encodeConfigData(newConfigData);
77 | console.log("New config data:", encodedNewConfigData);
78 |
79 | const newConfig = await buildNextConfig({
80 | account: args.account,
81 | currentConfigData: args.config_data,
82 | newConfigData: encodedNewConfigData,
83 | provider: masterClient,
84 | });
85 |
86 | console.log(`Setting new config with nonce ${newConfig.nonce}...`);
87 | const authorizationProof = await callsModule.signSetConfigAuth({
88 | account: args.account,
89 | config: newConfig,
90 | ownerIndex: 0n,
91 | privateKey,
92 | });
93 | const data = buildSetConfigCalldata(newConfig, authorizationProof);
94 |
95 | const calls: Call[] = [{
96 | index: 0,
97 | target: args.account,
98 | data,
99 | value: 0n,
100 | }];
101 |
102 | await callsModule.makeCalls({
103 | account: args.account,
104 | ownerIndex: args.owner_index,
105 | initialConfigData: args.initial_config_data,
106 | privateKey,
107 | calls,
108 | });
109 |
110 | console.log("Successfully changed configuration.");
111 | }
112 |
113 | if (import.meta.main) {
114 | main();
115 | }
116 |
--------------------------------------------------------------------------------
/scripts/create-p256-key.ts:
--------------------------------------------------------------------------------
1 | const P256 = require("ecdsa-secp256r1");
2 |
3 | console.log(JSON.stringify(P256.generateKey().toJWK()));
4 |
--------------------------------------------------------------------------------
/scripts/get-account.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentParser } from "argparse";
2 | import { defaultToEnv } from "./lib/argparse";
3 | import { getConfigDataForPrivateKey as getConfigDataForSecp256k1PrivateKey } from "../src/wallets/base-wallet/signers/secp256k1/config-data";
4 | import { getConfigDataForPrivateKey as getConfigDataForWebAuthnPrivateKey } from "../src/wallets/base-wallet/signers/webauthn/config-data";
5 | import { client } from "./lib/client";
6 | import { getAddress } from "../src/wallets/base-wallet/user-op";
7 | import { CoinbaseSmartWalletConfigData, encodeConfigData } from "../src/wallets/base-wallet/config";
8 | const P256 = require("ecdsa-secp256r1");
9 |
10 | async function main() {
11 | const parser = new ArgumentParser({
12 | description: "Get the keyspace key and account address for a given private key",
13 | });
14 |
15 | parser.add_argument("--private-key", {
16 | help: "The current private key of the owner",
17 | ...defaultToEnv("PRIVATE_KEY"),
18 | });
19 | parser.add_argument("--signature-type", {
20 | help: "The type of signature for the Keyspace key",
21 | default: "secp256k1",
22 | });
23 |
24 | const args = parser.parse_args();
25 | let configData: CoinbaseSmartWalletConfigData;
26 | if (args.signature_type === "secp256k1") {
27 | configData = getConfigDataForSecp256k1PrivateKey(args.private_key);
28 | } else if (args.signature_type === "webauthn") {
29 | const privateKey = P256.fromJWK(JSON.parse(args.private_key));
30 | configData = getConfigDataForWebAuthnPrivateKey(privateKey);
31 | } else {
32 | console.error("Invalid circuit type");
33 | return;
34 | }
35 |
36 | console.log("Initial config data:", encodeConfigData(configData));
37 | console.log("Account address:", await getAddress(client, {
38 | initialConfigData: encodeConfigData(configData),
39 | nonce: 0n,
40 | }));
41 | }
42 |
43 | if (import.meta.main) {
44 | main();
45 | }
46 |
--------------------------------------------------------------------------------
/scripts/lib/argparse.ts:
--------------------------------------------------------------------------------
1 | export function defaultToEnv(varName: string) {
2 | const value = process.env[varName];
3 | if (!value) {
4 | return { required: true };
5 | }
6 | return { default: value };
7 | }
8 |
--------------------------------------------------------------------------------
/scripts/lib/client.ts:
--------------------------------------------------------------------------------
1 | import { bundlerActions, BundlerClient } from "permissionless";
2 | import { createPublicClient, http, PublicClient } from "viem";
3 | import { baseSepolia, sepolia } from "viem/chains";
4 | import { publicActionsL2 } from 'viem/op-stack';
5 |
6 | export const chain = baseSepolia;
7 |
8 | export const client: PublicClient = createPublicClient({
9 | chain,
10 | transport: http(
11 | process.env.RPC_URL || ""
12 | ),
13 | }) as PublicClient;
14 |
15 | export const masterClient: PublicClient = client;
16 |
17 | export const l1Client: PublicClient = createPublicClient({
18 | chain: sepolia,
19 | transport: http(
20 | process.env.L1_RPC_URL || ""
21 | ),
22 | }).extend(publicActionsL2());
23 |
24 | export const bundlerClient: BundlerClient = createPublicClient({
25 | chain,
26 | transport: http(
27 | process.env.BUNDLER_RPC_URL || ""
28 | ),
29 | }).extend(bundlerActions);
30 |
--------------------------------------------------------------------------------
/scripts/send-eth.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentParser } from "argparse";
2 | import { defaultToEnv } from "./lib/argparse";
3 | import { Call } from "../src/wallets/base-wallet/user-op";
4 | import * as callsSecp256k1 from "../src/wallets/base-wallet/signers/secp256k1/calls";
5 | import * as callsWebAuthn from "../src/wallets/base-wallet/signers/webauthn/calls";
6 | const P256 = require("ecdsa-secp256r1");
7 |
8 | async function main() {
9 | const parser = new ArgumentParser({
10 | description: "Send 1 wei",
11 | });
12 |
13 | parser.add_argument("--account", {
14 | help: "The account of the keystore wallet to send from",
15 | required: true,
16 | });
17 | parser.add_argument("--owner-index", {
18 | help: "The index of the owner",
19 | default: 0,
20 | });
21 | parser.add_argument("--initial-config-data", {
22 | help: "The initial config data needed to deploy the wallet as a hex string",
23 | });
24 | parser.add_argument("--private-key", {
25 | help: "The current private key of the owner",
26 | ...defaultToEnv("PRIVATE_KEY"),
27 | });
28 | parser.add_argument("--to", {
29 | help: "The address to send to",
30 | required: true,
31 | });
32 | parser.add_argument("--signature-type", {
33 | help: "The type of signature for the signing key",
34 | default: "secp256k1",
35 | });
36 |
37 | const args = parser.parse_args();
38 | let callsModule: any;
39 | let privateKey: any;
40 | if (args.signature_type === "secp256k1") {
41 | console.log("Using secp256k1 via keyspace...");
42 | callsModule = callsSecp256k1;
43 | privateKey = args.private_key;
44 | } else if (args.signature_type === "webauthn") {
45 | console.log("Using WebAuthn via keyspace...");
46 | callsModule = callsWebAuthn;
47 | privateKey = P256.fromJWK(JSON.parse(args.private_key));
48 | } else {
49 | console.error("Invalid circuit type");
50 | }
51 |
52 | const amount = 1n;
53 | const calls: Call[] = [{
54 | index: 0,
55 | target: args.to,
56 | data: "0x",
57 | value: amount,
58 | }];
59 | callsModule.makeCalls({
60 | account: args.account,
61 | ownerIndex: args.owner_index,
62 | initialConfigData: args.initial_config_data,
63 | privateKey,
64 | calls,
65 | });
66 | }
67 |
68 | if (import.meta.main) {
69 | main();
70 | }
71 |
--------------------------------------------------------------------------------
/scripts/sync-keystore.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentParser } from "argparse";
2 | import { defaultToEnv } from "./lib/argparse";
3 | import { encodeOPStackProof, getMasterKeystoreProofs } from "../src/proofs/op-stack";
4 | import { l1Client } from "./lib/client";
5 | import { createPublicClient, fromHex, http, PublicClient } from "viem";
6 | import * as chains from "viem/chains";
7 | import { getIsDeployed, getMasterChainId } from "../src/wallets/base-wallet/contract";
8 | import * as callsSecp256k1 from "../src/wallets/base-wallet/signers/secp256k1/calls";
9 | import * as callsWebAuthn from "../src/wallets/base-wallet/signers/webauthn/calls";
10 | import { buildSyncConfigCalldata, hashConfig } from "../src/config";
11 | import { Call } from "../src/wallets/base-wallet/user-op";
12 | const P256 = require("ecdsa-secp256r1");
13 |
14 | async function main() {
15 | const parser = new ArgumentParser({
16 | description: "Sync the wallet's keystore config from the configured master chain to the replica chain",
17 | });
18 |
19 | parser.add_argument("--account", {
20 | help: "The account of the keystore wallet to sync",
21 | required: true,
22 | });
23 | parser.add_argument("--private-key", {
24 | help: "The current private key of the syncer",
25 | ...defaultToEnv("PRIVATE_KEY"),
26 | });
27 | parser.add_argument("--signature-type", {
28 | help: "The type of signature for the private key",
29 | default: "secp256k1",
30 | });
31 | parser.add_argument("--config-data", {
32 | help: "The current config data for the wallet to sync as a hex string",
33 | required: true,
34 | });
35 | parser.add_argument("--initial-config-data", {
36 | help: "The initial config data needed to deploy the wallet as a hex string. Required if the wallet has not been deployed.",
37 | });
38 | parser.add_argument("--target-chain", {
39 | help: "The target chain to sync the wallet to",
40 | default: "OP Sepolia",
41 | });
42 |
43 | const args = parser.parse_args();
44 |
45 | let privateKey: any;
46 | let callsModule: any;
47 | if (args.signature_type === "secp256k1") {
48 | console.log("Using secp256k1 private key...");
49 | privateKey = args.private_key;
50 | callsModule = callsSecp256k1;
51 | } else if (args.signature_type === "webauthn") {
52 | console.log("Using WebAuthn private key...");
53 | privateKey = P256.fromJWK(JSON.parse(args.private_key));
54 | callsModule = callsWebAuthn;
55 | } else {
56 | console.error("Invalid signature type");
57 | }
58 |
59 | // Using the data on the specified replica chain, detect the master chain ID.
60 | const replicaChain = Object.values(chains).find((chain) => chain.name === args.target_chain);
61 | const replicaClient: PublicClient = createPublicClient({
62 | chain: replicaChain,
63 | transport: http(),
64 | }) as PublicClient;
65 |
66 | const masterChainId = await getMasterChainId(replicaClient);
67 | const masterChain = Object.values(chains).find((chain) => BigInt(chain.id) === masterChainId);
68 | const masterClient: PublicClient = createPublicClient({
69 | chain: masterChain,
70 | transport: http(),
71 | }) as PublicClient;
72 |
73 | // Query the master chain and L1 for proofs of the config hash.
74 | const keystoreProofs = await getMasterKeystoreProofs(args.account, masterClient, replicaClient, l1Client);
75 |
76 | // Encode the nonce and --config-data into a Config struct, then hash it and
77 | // compare it to the proof.
78 | const currentConfig = {
79 | account: args.account,
80 | nonce: keystoreProofs.keystoreConfigNonce,
81 | data: args.config_data,
82 | };
83 | const currentConfigHash = hashConfig(currentConfig);
84 | if (currentConfigHash !== keystoreProofs.keystoreConfigHash) {
85 | if (fromHex(keystoreProofs.keystoreConfigHash, "bigint") === 0n) {
86 | console.log("The config hash is empty on the master chain. Syncing the confirmed config timestamp...");
87 | } else {
88 | console.warn(`The provided config data does not hash to the expected value. Expected ${keystoreProofs.keystoreConfigHash}, got ${currentConfigHash}.`);
89 | }
90 | }
91 |
92 | // Check if we need to deploy the wallet before syncing.
93 | if (!await getIsDeployed(replicaClient, args.account) && !args.initial_config_data) {
94 | console.error("Wallet is not deployed, and no initial config data was provided.");
95 | process.exit(1);
96 | }
97 |
98 | // Call syncConfig on the replica chain with the Config struct and the proof.
99 | const keystoreProof = encodeOPStackProof(keystoreProofs);
100 | const data = buildSyncConfigCalldata(currentConfig, keystoreProof);
101 |
102 | const calls: Call[] = [{
103 | index: 0,
104 | target: args.account,
105 | data,
106 | value: 0n,
107 | }];
108 |
109 | await callsModule.makeCalls({
110 | account: args.account,
111 | ownerIndex: args.owner_index,
112 | initialConfigData: args.initial_config_data,
113 | privateKey,
114 | calls,
115 | });
116 |
117 | }
118 |
119 | if (import.meta.main) {
120 | main();
121 | }
122 |
--------------------------------------------------------------------------------
/scripts/verify-p256-signature.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentParser } from "argparse";
2 | import { Hex, hexToBytes } from "viem";
3 |
4 | const P256 = require("ecdsa-secp256r1");
5 |
6 | /**
7 | * Verifies an ECDSA-secp256r1 signature given:
8 | * - 64-byte uncompressed public key (x||y) in hex
9 | * - Message bytes in hex
10 | * - Signature (r||s) in hex
11 | *
12 | * Returns true if valid, false otherwise.
13 | */
14 | function verifySignature(pubKeyHex: Hex, messageHex: Hex, signatureHex: Hex) {
15 | const publicKey = decodePublicKey(pubKeyHex);
16 | const messageBuffer = Buffer.from(messageHex.slice(2), 'hex');
17 |
18 | const signature = Buffer.from(signatureHex.slice(2), 'hex');
19 | console.log("r", signature.subarray(0, 32).toString('hex'));
20 | console.log("s", signature.subarray(32).toString('hex'));
21 |
22 | return publicKey.verify(messageBuffer, signatureHex.slice(2), 'hex');
23 | }
24 |
25 | function decodePublicKey(pubKeyHex: Hex) {
26 | const bytes = hexToBytes(pubKeyHex);
27 | const x = bytes.slice(0, 32);
28 | const y = bytes.slice(32, 64);
29 | return new P256({ x, y });
30 | }
31 |
32 | async function main() {
33 | const parser = new ArgumentParser({
34 | description: "Verify a P256 signature",
35 | });
36 |
37 | parser.add_argument("--public-key", {
38 | help: "The public key of the signer",
39 | required: true,
40 | });
41 | parser.add_argument("--signature", {
42 | help: "The P256 signature to verify",
43 | required: true,
44 | });
45 | parser.add_argument("--message", {
46 | help: "The message to verify",
47 | required: true,
48 | });
49 |
50 | const args = parser.parse_args();
51 | const result = verifySignature(args.public_key, args.message, args.signature);
52 | console.log(result);
53 | }
54 |
55 | if (import.meta.main) {
56 | main();
57 | }
58 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import { PublicClient, createPublicClient, custom, fromHex } from "viem";
2 | import * as chains from "viem/chains";
3 |
4 | export type EthereumProvider = { request(...args: any): Promise; };
5 |
6 | export type ProviderClientConfig = EthereumProvider & {
7 | chain: {
8 | id: number;
9 | } | undefined;
10 | }
11 |
12 | export function createCustomClient(provider: ProviderClientConfig): PublicClient {
13 | const chain = Object.values(chains).find((chain) => chain.id === provider.chain?.id);
14 | if (!chain) {
15 | throw new Error("Chain not found");
16 | }
17 |
18 | return createPublicClient({
19 | chain,
20 | transport: custom(provider)
21 | }) as PublicClient;
22 | }
23 |
24 | export async function createProviderClientConfig(provider: EthereumProvider): Promise {
25 | const chainIdResponse = await provider.request({ method: "eth_chainId" });
26 | const chainId = fromHex(chainIdResponse, "number");
27 |
28 | return {
29 | chain: {
30 | id: chainId,
31 | },
32 | ...provider,
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { Address, encodeAbiParameters, encodeFunctionData, encodePacked, fromHex, Hex, keccak256, PublicClient, toHex } from "viem";
2 | import { accountAbi } from "@generated";
3 | import { MASTER_KEYSTORE_STORAGE_LOCATION } from "@/proofs/op-stack";
4 | import { createCustomClient, ProviderClientConfig } from "@/client";
5 |
6 | export type KeystoreConfig = {
7 | account: Address;
8 | nonce: bigint;
9 | data: Hex;
10 | }
11 |
12 | export type MasterKeystoreStorage = {
13 | configHash: Hex;
14 | configNonce: bigint;
15 | };
16 |
17 | export function encodeConfig({ account, nonce, data }: KeystoreConfig): Hex {
18 | return encodePacked(
19 | ["address", "uint256", "bytes"],
20 | [account, nonce, data]
21 | );
22 | }
23 |
24 | export function hashConfig({ account, nonce, data }: KeystoreConfig): Hex {
25 | return keccak256(encodeConfig({ account, nonce, data }));
26 | }
27 |
28 | /**
29 | * Retrieves the current nonce for the given keystore account.
30 | *
31 | * @param client - The PublicClient instance connected to the master chain.
32 | * @param account - The address of the keystore account to get the nonce for.
33 | * @returns The current nonce for the keystore account.
34 | */
35 |
36 | export async function getMasterKeystoreStorage(provider: ProviderClientConfig, account: Address): Promise {
37 | const client = createCustomClient(provider);
38 | const configHashSlot = toHex(fromHex(MASTER_KEYSTORE_STORAGE_LOCATION, "bigint") + 0n);
39 | const configNonceSlot = toHex(fromHex(MASTER_KEYSTORE_STORAGE_LOCATION, "bigint") + 1n);
40 |
41 | return {
42 | configHash: await client.getStorageAt({
43 | address: account,
44 | slot: configHashSlot,
45 | }) ?? "0x",
46 | configNonce: fromHex(await client.getStorageAt({
47 | address: account,
48 | slot: configNonceSlot,
49 | }) ?? "0x", "bigint"),
50 | };
51 | }
52 |
53 | export function buildSetConfigCalldata(config: KeystoreConfig, authorizationProof: Hex) {
54 | return encodeFunctionData({
55 | abi: accountAbi,
56 | functionName: "setConfig",
57 | args: [config, authorizationProof],
58 | });
59 | }
60 |
61 | export function buildSyncConfigCalldata(config: KeystoreConfig, keystoreProof: Hex) {
62 | return encodeFunctionData({
63 | abi: accountAbi,
64 | functionName: "syncConfig",
65 | args: [config, keystoreProof],
66 | });
67 | }
68 |
69 | type BuildNextConfigArgs = {
70 | account: Address;
71 | currentConfigData: Hex;
72 | newConfigData: Hex;
73 | provider: ProviderClientConfig;
74 | };
75 |
76 | export async function buildNextConfig({ account, currentConfigData, newConfigData, provider }: BuildNextConfigArgs): Promise {
77 | const { configHash, configNonce } = await getMasterKeystoreStorage(provider, account);
78 | if (fromHex(configHash, "bigint") !== 0n) {
79 | const expectedConfigHash = hashConfig({ account, nonce: configNonce, data: currentConfigData });
80 | if (configHash !== expectedConfigHash) {
81 | throw new Error(`Config hash mismatch: actual ${configHash} !== expected ${expectedConfigHash}`);
82 | }
83 | }
84 |
85 | return {
86 | account,
87 | nonce: configNonce + 1n,
88 | data: newConfigData,
89 | };
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/src/proofs/op-stack.ts:
--------------------------------------------------------------------------------
1 | import { PublicClient, type Hex, keccak256, encodeAbiParameters, toHex, toRlp, Address, fromHex } from "viem";
2 | import { readContract } from "viem/actions";
3 | import { anchorStateRegistryAbi, anchorStateRegistryAddress, l1BlockAbi, l1BlockAddress } from "@generated";
4 | import { createCustomClient, ProviderClientConfig } from "@/client";
5 |
6 | type CrossChainProofBlockNumbers = {
7 | masterBlockNumber: bigint;
8 | l1BlockNumber: bigint;
9 | }
10 |
11 | export const MASTER_KEYSTORE_STORAGE_LOCATION = "0xab0db9dff4dd1cc7cbf1b247b1f1845c685dfd323fb0c6da795f47e8940a2c00";
12 |
13 | /**
14 | * Retrieves the master chain block number for the latest storage root confirmed on the L1 chain.
15 | */
16 | export async function getCrossChainProofBlockNumbers(l1Client: PublicClient, replicaClient: PublicClient, replicaBlockNumber: bigint): Promise {
17 | // On the replica, get the L1 block number for the latest storage root.
18 | const l1BlockNumber = await readContract(replicaClient, {
19 | abi: l1BlockAbi,
20 | address: l1BlockAddress,
21 | functionName: "number",
22 | blockNumber: replicaBlockNumber,
23 | });
24 |
25 | // On L1, get the master chain's confirmed block number at the given L1 block number.
26 | const [_, masterBlockNumber] = await readContract(l1Client, {
27 | abi: anchorStateRegistryAbi,
28 | address: anchorStateRegistryAddress,
29 | functionName: "anchors",
30 | args: [0],
31 | blockNumber: l1BlockNumber,
32 | });
33 |
34 | return { masterBlockNumber, l1BlockNumber };
35 | }
36 |
37 | export type OPStackMasterKeystoreProofs = {
38 | l1BlockHeaderRlp: Hex;
39 | l1BlockHashProof: L1BlockHashProof;
40 | anchorStateRegistryAccountProof: Hex[];
41 | anchorStateRegistryStorageProof: Hex[];
42 | masterKeystoreAccountProof: Hex[];
43 | masterKeystoreStorageProof: Hex[];
44 | keystoreConfigHash: Hex;
45 | keystoreConfigNonce: bigint;
46 | l2StateRoot: Hex;
47 | l2MessagePasserStorageRoot: Hex;
48 | l2BlockHash: Hex;
49 | };
50 |
51 | /**
52 | * Retrieves a proof of the latest keystore storage root on the master chain that has been
53 | * confirmed on the L1 chain.
54 | *
55 | * @param account - The address of the keystore account to prove.
56 | * @param l1Client - The PublicClient instance connected to the L1 chain.
57 | * @param masterClient - The PublicClient instance connected to the master chain.
58 | * @param replicaClient - The PublicClient instance connected to the replica chain.
59 | * @returns A keystore storage root proof object containing various proofs and state roots.
60 | */
61 | export async function getMasterKeystoreProofs(account: Address, masterClient: PublicClient, replicaClient: PublicClient, l1Client: PublicClient): Promise {
62 | // Start from the previous block on the replica chain to avoid issues on
63 | // forked chains that don't increment blocks. From there, find the block
64 | // numbers on the master and L1 chains to use for our proofs.
65 | const replicaBlockNumber = await replicaClient.getBlockNumber() - 1n;
66 | const { masterBlockNumber, l1BlockNumber } = await getCrossChainProofBlockNumbers(l1Client, replicaClient, replicaBlockNumber);
67 | const l1BlockHeaderRlp = await getBlockHeaderRlp(l1Client, l1BlockNumber);
68 |
69 | // Prove the anchor state registry account on the L1 chain.
70 | const anchorStorageProof = await getAnchorStateRegistryProof(l1Client, l1BlockNumber);
71 | const anchorStateRegistryAccountProof = anchorStorageProof.accountProof;
72 | const anchorStateRegistryStorageProof = anchorStorageProof.storageProof[0].proof;
73 |
74 | // Prove the master keystore storage slots.
75 | const configHashSlot = toHex(fromHex(MASTER_KEYSTORE_STORAGE_LOCATION, "bigint") + 0n);
76 | const configNonceSlot = toHex(fromHex(MASTER_KEYSTORE_STORAGE_LOCATION, "bigint") + 1n);
77 | const keystoreProof = await masterClient.getProof({
78 | address: account,
79 | storageKeys: [configHashSlot, configNonceSlot],
80 | blockNumber: masterBlockNumber,
81 | });
82 | const masterKeystoreAccountProof = keystoreProof.accountProof;
83 | const masterKeystoreStorageProof = keystoreProof.storageProof[0].proof;
84 | const keystoreConfigHash = toHex(keystoreProof.storageProof[0].value);
85 | const keystoreConfigNonce = keystoreProof.storageProof[1].value;
86 |
87 |
88 | const outputRootPreimages = await getOutputRootPreimages(masterClient, masterBlockNumber);
89 |
90 | const opStackL1BlockProof = await getOPStackL1BlockProof(replicaClient, replicaBlockNumber);
91 | const l1BlockHashProof = encodeOPStackL1BlockProof(opStackL1BlockProof);
92 |
93 |
94 | return {
95 | l1BlockHeaderRlp,
96 | l1BlockHashProof,
97 | anchorStateRegistryAccountProof,
98 | anchorStateRegistryStorageProof,
99 | masterKeystoreAccountProof,
100 | masterKeystoreStorageProof,
101 | keystoreConfigHash,
102 | keystoreConfigNonce,
103 | l2StateRoot: outputRootPreimages.stateRoot,
104 | l2MessagePasserStorageRoot: outputRootPreimages.messagePasserStorageRoot,
105 | l2BlockHash: outputRootPreimages.hash,
106 | };
107 | }
108 |
109 | export function encodeOPStackProof(proof: OPStackMasterKeystoreProofs): Hex {
110 | return encodeAbiParameters(
111 | [{ type: "tuple", name: "proof", components: [
112 | { type: "bytes", name: "l1BlockHeaderRlp" },
113 | { type: "tuple", name: "l1BlockHashProof", components: [
114 | { type: "uint8", name: "proofType" },
115 | { type: "bytes", name: "proofData" },
116 | ]},
117 | { type: "bytes[]", name: "masterKeystoreAccountProof" },
118 | { type: "bytes[]", name: "masterKeystoreStorageProof" },
119 | { type: "bytes[]", name: "anchorStateRegistryAccountProof" },
120 | { type: "bytes[]", name: "anchorStateRegistryStorageProof" },
121 | { type: "bytes", name: "l2StateRoot" },
122 | { type: "bytes", name: "l2MessagePasserStorageRoot" },
123 | { type: "bytes", name: "l2BlockHash" },
124 | ]}],
125 | [proof]
126 | );
127 | }
128 |
129 | type OPStackProofData = {
130 | l2BlockHeaderRlp: Hex;
131 | l1BlockAccountProof: Hex[];
132 | l1BlockStorageProof: Hex[];
133 | };
134 |
135 | /**
136 | * Proves the L1Block.hash storage slot on an OP Stack chain.
137 | *
138 | * @param client - The PublicClient instance used to interact with the blockchain.
139 | * @param blockNumber - The block number for which to retrieve the proof data.
140 | * @returns A promise that resolves to an object containing the L2 block header RLP,
141 | * the L1 block account proof, and the L1 block storage proof.
142 | */
143 | async function getOPStackL1BlockProof(provider: ProviderClientConfig, blockNumber: bigint): Promise {
144 | const client = createCustomClient(provider);
145 | const l2BlockHeaderRlp = await getBlockHeaderRlp(client, blockNumber);
146 | // cast storage 0x4200000000000000000000000000000000000015 --rpc-url https://sepolia.base.org
147 | const l1BlockHashSlot = BigInt(2);
148 | const l1BlockProof = await client.getProof({
149 | address: l1BlockAddress,
150 | storageKeys: [toHex(l1BlockHashSlot, { size: 32 })],
151 | blockNumber: blockNumber,
152 | });
153 | const l1BlockAccountProof = l1BlockProof.accountProof;
154 | const l1BlockStorageProof = l1BlockProof.storageProof[0].proof;
155 |
156 | return {
157 | l2BlockHeaderRlp,
158 | l1BlockAccountProof,
159 | l1BlockStorageProof,
160 | };
161 | }
162 |
163 | type L1BlockHashProof = {
164 | proofType: 1;
165 | proofData: Hex;
166 | };
167 |
168 | /**
169 | * Encodes an OP Stack L1 block proof into the struct expected by the keystore.
170 | *
171 | * @param l1BlockProof - The OP Stack proof data to be encoded.
172 | * @returns An ABI-encoded OPStackL1BlockProof struct.
173 | */
174 | function encodeOPStackL1BlockProof(l1BlockProof: OPStackProofData): L1BlockHashProof {
175 | return {
176 | proofType: 1,
177 | proofData: encodeAbiParameters(
178 | [{
179 | components: [
180 | {
181 | name: "l2BlockHeaderRlp",
182 | type: "bytes",
183 | },
184 | {
185 | name: "l1BlockAccountProof",
186 | type: "bytes[]",
187 | },
188 | {
189 | name: "l1BlockStorageProof",
190 | type: "bytes[]",
191 | }
192 | ],
193 | type: "tuple",
194 | }],
195 | [l1BlockProof]
196 | ),
197 | };
198 | }
199 |
200 | /**
201 | * Reconstructs the block header in RLP (Recursive Length Prefix) encoding for a given block number.
202 | *
203 | * Assumes the block header format for Cancun/Deneb blocks.
204 | * WARNING: This will break when the next hard fork adds new fields to the header.
205 | *
206 | * @param client - An instance of `PublicClient` used to fetch the block data.
207 | * @param blockNumber - The block number for which the header is to be retrieved.
208 | * @returns A promise that resolves to the RLP encoded block header.
209 | *
210 | * @throws Will throw an error if the block header hash does not match the expected hash.
211 | */
212 |
213 | async function getBlockHeaderRlp(provider: ProviderClientConfig, blockNumber: bigint) {
214 | const client = createCustomClient(provider);
215 | const blockHeader = await client.getBlock({ blockNumber });
216 | const fields: Hex[] = [
217 | blockHeader.parentHash,
218 | blockHeader.sha3Uncles,
219 | blockHeader.miner,
220 | blockHeader.stateRoot,
221 | blockHeader.transactionsRoot,
222 | blockHeader.receiptsRoot,
223 | blockHeader.logsBloom,
224 | toHex(blockHeader.difficulty),
225 | toHex(blockHeader.number),
226 | toHex(blockHeader.gasLimit),
227 | toHex(blockHeader.gasUsed),
228 | toHex(blockHeader.timestamp),
229 | blockHeader.extraData,
230 | blockHeader.mixHash,
231 | blockHeader.nonce,
232 | toHex(blockHeader.baseFeePerGas || 0),
233 | blockHeader.withdrawalsRoot || "0x",
234 | toHex(blockHeader.blobGasUsed),
235 | toHex(blockHeader.excessBlobGas),
236 | blockHeader.parentBeaconBlockRoot || "0x",
237 | ].map((field) => /^0x0$/.test(field) ? "0x" : field);
238 | const rawHeader = toRlp(fields);
239 | const hash = keccak256(rawHeader);
240 | console.assert(hash === blockHeader.hash, "Block header hash mismatch");
241 | return rawHeader;
242 | }
243 |
244 | /**
245 | * Proves the master chain output root at the given L1 block number.
246 | *
247 | * @param l1Client - The public client to interact with the L1 blockchain.
248 | * @param l1BlockNumber - The block number on the L1 blockchain to get the proof at.
249 | * @returns A promise that resolves to the proof of the master chain output root.
250 | */
251 | async function getAnchorStateRegistryProof(l1Client: PublicClient, l1BlockNumber: bigint) {
252 | // Prove the master chain output root at the given L1 block number.
253 | // cast storage 0x95907b5069e5a2ef1029093599337a6c9dac8923 --rpc-url https://rpc.sepolia.org
254 | const anchorsSlot = BigInt(1);
255 | return await l1Client.getProof({
256 | address: anchorStateRegistryAddress,
257 | storageKeys: [keccak256(encodeAbiParameters([
258 | { type: "uint256" },
259 | { type: "uint256" },
260 | ], [
261 | BigInt(0),
262 | anchorsSlot,
263 | ]))],
264 | blockNumber: l1BlockNumber,
265 | });
266 | }
267 |
268 | /**
269 | * Retrieves the output root preimage for a given L2 block number.
270 | *
271 | * The output root preimage allows us to reconstruct the output root and trust the
272 | * L2 block hash within it.
273 | *
274 | * @param client - The public client instance used to interact with the blockchain.
275 | * @param blockNumber - The block number for which to retrieve the output root preimage.
276 | * @returns An object containing the state root, block hash, and message passer storage root.
277 | */
278 | async function getOutputRootPreimages(provider: ProviderClientConfig, blockNumber: bigint) {
279 | const client = createCustomClient(provider);
280 | const messagePasserAddress = "0x4200000000000000000000000000000000000016";
281 | const messagePasserProof = await client.getProof({
282 | address: messagePasserAddress,
283 | storageKeys: [],
284 | blockNumber,
285 | });
286 | const masterBlock = await client.getBlock({ blockNumber });
287 |
288 | return {
289 | stateRoot: masterBlock.stateRoot,
290 | hash: masterBlock.hash,
291 | messagePasserStorageRoot: messagePasserProof.storageHash,
292 | };
293 | }
294 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/config.ts:
--------------------------------------------------------------------------------
1 | import { type Hex, decodeAbiParameters, encodeAbiParameters } from "viem";
2 | import { KeystoreConfig } from "@/config";
3 |
4 |
5 | export type CoinbaseSmartWalletConfigData = {
6 | owners: Hex[];
7 | }
8 |
9 | /**
10 | * Packs the owner bytes into the full Base Wallet keystore config data format.
11 | *
12 | * @param ownerBytes - The hexadecimal representation of a single owner's bytes.
13 | * @returns The encoded keystore record storage.
14 | */
15 | export function encodeConfigData(configData: CoinbaseSmartWalletConfigData): Hex {
16 | return encodeAbiParameters([{
17 | components: [
18 | { name: "owners", type: "bytes[]" },
19 | ],
20 | type: "tuple",
21 | }], [configData]);
22 | }
23 |
24 | export function decodeConfigData(configData: Hex): CoinbaseSmartWalletConfigData {
25 | const decoded = decodeAbiParameters([{
26 | components: [{ name: "owners", type: "bytes[]" }],
27 | type: "tuple",
28 | }], configData)[0];
29 | return {
30 | owners: [...decoded.owners]
31 | };
32 | }
33 |
34 | export function getConfigDataWithAddedOwner(configData: CoinbaseSmartWalletConfigData, ownerBytes: Hex): CoinbaseSmartWalletConfigData {
35 | return {
36 | ...configData,
37 | owners: [...configData.owners, ownerBytes],
38 | };
39 | }
40 |
41 | export function getConfigDataWithRemovedOwner(configData: CoinbaseSmartWalletConfigData, ownerBytes: Hex): CoinbaseSmartWalletConfigData {
42 | return {
43 | ...configData,
44 | owners: configData.owners.filter((o) => o !== ownerBytes),
45 | };
46 | }
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/contract.ts:
--------------------------------------------------------------------------------
1 | import { Address, encodeFunctionData, PublicClient } from "viem";
2 | import { getCode, readContract } from "viem/actions";
3 | import { accountAbi, accountFactoryAbi, accountFactoryAddress } from "@generated";
4 | import { createCustomClient, ProviderClientConfig } from "@/client";
5 |
6 | export async function getIsDeployed(provider: ProviderClientConfig, address: Address): Promise {
7 | const client = createCustomClient(provider);
8 | const codeAtAddress = await getCode(client, { address });
9 | return codeAtAddress != undefined && codeAtAddress !== "0x";
10 | }
11 |
12 | export async function getMasterChainId(provider: ProviderClientConfig) {
13 | const client = createCustomClient(provider);
14 | if (!client.chain?.id) {
15 | throw new Error("Chain not found");
16 | }
17 |
18 | const implementation = await readContract(client, {
19 | address: accountFactoryAddress[client.chain.id as keyof typeof accountFactoryAddress],
20 | abi: accountFactoryAbi,
21 | functionName: "implementation",
22 | });
23 |
24 | const masterChainId = await readContract(client, {
25 | address: implementation,
26 | abi: accountAbi,
27 | functionName: "masterChainId",
28 | });
29 |
30 | return masterChainId;
31 | }
32 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/secp256k1/calls.ts:
--------------------------------------------------------------------------------
1 | import { Address, encodeAbiParameters, Hex } from "viem";
2 |
3 | import { entryPointAddress } from "@generated";
4 | import { getConfigDataForPrivateKey } from "./config-data";
5 | import { buildUserOp, Call, getUserOpHash } from "@/wallets/base-wallet/user-op";
6 | import { client, chain, bundlerClient } from "@scripts/lib/client";
7 | import { signAndWrap } from "./sign";
8 | import { encodeConfigData } from "@/wallets/base-wallet/config";
9 | import { buildDummySignature } from "./signatures";
10 | import { hashConfig, KeystoreConfig } from "@/config";
11 |
12 | export type MakeCallsParameters = {
13 | account: Address;
14 | ownerIndex: bigint;
15 | calls: Call[];
16 | paymasterAndData?: Hex;
17 | initialConfigData?: Hex;
18 | privateKey: Hex;
19 | }
20 |
21 | /**
22 | * Creates and sends a Base Wallet user operation signed with an secp256k1 private key.
23 | *
24 | * @param account - The address of the account.
25 | * @param calls - An array of calls to be executed.
26 | * @param privateKey - The hexadecimal private key used for signing.
27 | * @param paymasterData - Optional hexadecimal data for the paymaster. Defaults to "0x".
28 | * @returns A promise of the user operation hash.
29 | */
30 | export async function makeCalls({ account, ownerIndex, calls, privateKey, paymasterAndData, initialConfigData }: MakeCallsParameters) {
31 | initialConfigData ??= encodeConfigData(getConfigDataForPrivateKey(privateKey));
32 | const op = await buildUserOp({
33 | account,
34 | initialConfigData,
35 | calls,
36 | paymasterAndData: paymasterAndData ?? "0x",
37 | dummySignature: buildDummySignature(),
38 | provider: client,
39 | bundlerProvider: bundlerClient,
40 | });
41 |
42 | const hash = getUserOpHash({ userOperation: op, chainId: BigInt(chain.id) });
43 | op.signature = await signAndWrap({ hash, ownerIndex, privateKey });
44 |
45 | const opHash = await bundlerClient.sendUserOperation({
46 | userOperation: op,
47 | entryPoint: entryPointAddress,
48 | });
49 |
50 | return opHash;
51 | }
52 |
53 | /**
54 | * Signs the authorization for a setConfig transaction.
55 | *
56 | * @param config - The new configuration data.
57 | * @param ownerIndex - The index of the owner.
58 | * @param privateKey - The private key object used for signing.
59 | * @returns A promise of the encoded authorization signature.
60 | */
61 | export async function signSetConfigAuth({
62 | config,
63 | ownerIndex,
64 | privateKey,
65 | }: {
66 | config: KeystoreConfig,
67 | ownerIndex: bigint,
68 | privateKey: Hex,
69 | }) {
70 | const hash = hashConfig(config);
71 | const sigAuth = await signAndWrap({ hash, privateKey, ownerIndex });
72 | const sigUpdate = "0x";
73 | return encodeAbiParameters(
74 | [
75 | { type: "bytes", name: "sigAuth" },
76 | { type: "bytes", name: "sigUpdate" },
77 | ],
78 | [sigAuth, sigUpdate]
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/secp256k1/config-data.ts:
--------------------------------------------------------------------------------
1 | import { type Hex, encodeAbiParameters, keccak256 } from "viem";
2 | import { CoinbaseSmartWalletConfigData } from "../../config";
3 | import { privateKeyToAccount } from "viem/accounts";
4 |
5 |
6 | /**
7 | * Encodes the given private key as the raw bytes of the Base Wallet config data format.
8 | *
9 | * @param privateKey - The private key as a hex string.
10 | * @returns The config data as a hex string.
11 | */
12 | export function getConfigDataForPrivateKey(privateKey: Hex): CoinbaseSmartWalletConfigData {
13 | const ownerBytes = serializePublicKeyFromPrivateKey(privateKey);
14 | return { owners: [ownerBytes] };
15 | }
16 |
17 | /**
18 | * Serializes a raw secp256k1 public key into an Ethereum address.
19 | *
20 | * @param publicKey - The public key as a Uint8Array.
21 | * @returns The address padded to 32 bytes.
22 | */
23 | export function serializePublicKeyFromBytes(publicKey: Uint8Array): Hex {
24 | const keyHash = keccak256(publicKey);
25 | const address = `0x${keyHash.slice(2, 42)}` as Hex;
26 | return encodeAbiParameters([{ type: "address" }], [address]);
27 | }
28 |
29 | /**
30 | * Serializes a secp256k1 private key into an Ethereum address.
31 | *
32 | * @param privateKey - The private key.
33 | * @returns The address padded to 32 bytes.
34 | */
35 | export function serializePublicKeyFromPrivateKey(privateKey: Hex): Hex {
36 | const account = privateKeyToAccount(privateKey);
37 | return encodeAbiParameters([{ type: "address" }], [account.address]);
38 | }
39 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/secp256k1/sign.ts:
--------------------------------------------------------------------------------
1 | import { Hex } from "viem";
2 | import { sign } from "viem/accounts";
3 | import { wrapSignature } from "../../user-op";
4 | import { encodePackedSignature } from "./signatures";
5 |
6 | export type SignAndWrapParameters = {
7 | hash: Hex;
8 | ownerIndex: bigint;
9 | privateKey: Hex;
10 | };
11 |
12 | /**
13 | * Signs a hash with the provided private key and wraps the signature for use by the Base Wallet contracts.
14 | *
15 | * @param hash - The hash to be signed.
16 | * @param privateKey - The secp256k1 private key used for signing.
17 | * @param keystoreID - The keystore ID associated with the Base Wallet.
18 | * @returns A promise that resolves to the wrapped signature as a hex string.
19 | */
20 | export async function signAndWrap({ hash, ownerIndex, privateKey }: SignAndWrapParameters): Promise {
21 | const signature = await sign({ hash, privateKey });
22 | return wrapSignature(ownerIndex, encodePackedSignature(signature));
23 | }
24 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/secp256k1/signatures.ts:
--------------------------------------------------------------------------------
1 | import {
2 | encodePacked,
3 | type Hex,
4 | toHex,
5 | fromHex,
6 | } from "viem";
7 | import { SignReturnType } from "viem/accounts";
8 | import { wrapSignature } from "../../user-op";
9 |
10 |
11 | /**
12 | * Builds a dummy signature for estimating the gas cost of user operations.
13 | *
14 | * @returns {Uint8Array} The encoded dummy signature.
15 | */
16 | export function buildDummySignature() {
17 | const dummyPublicKey = new Uint8Array(65);
18 | dummyPublicKey[0] = 4;
19 | return wrapSignature(0n, encodePackedSignature({
20 | r: "0x0000000000000000000000000000000000000000000000000000000000000000",
21 | s: "0x0000000000000000000000000000000000000000000000000000000000000000",
22 | v: 0n,
23 | }));
24 | }
25 |
26 | /**
27 | * Encodes a secp256k1 signature into the packed Hex value expected by the Base Wallet contracts.
28 | *
29 | * @param signature - The signature to encode.
30 | * @returns The encoded signature.
31 | */
32 | export function encodePackedSignature(signature: SignReturnType): Hex {
33 | return encodePacked(
34 | ["bytes32", "bytes32", "uint8"],
35 | [
36 | // Viem's sign function returns Hex values for r and s without specifying,
37 | // a size, which occasionally produces 63-character Hex values that
38 | // encodePacked will reject.
39 | toHex(fromHex(signature.r, "bigint"), { size: 32 }),
40 | toHex(fromHex(signature.s, "bigint"), { size: 32 }),
41 | parseInt((signature.v || 0).toString()),
42 | ],
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/webauthn/calls.ts:
--------------------------------------------------------------------------------
1 | import { Address, encodeAbiParameters, Hex } from "viem";
2 | const P256 = require("ecdsa-secp256r1");
3 |
4 | import { entryPointAddress } from "@generated";
5 | import { getConfigDataForPrivateKey } from "./config-data";
6 | import { P256PrivateKey, signAndWrap } from "./sign";
7 | import { buildUserOp, Call, getUserOpHash } from "@/wallets/base-wallet/user-op";
8 | import { bundlerClient, chain, client } from "@scripts/lib/client";
9 | import { encodeConfigData } from "@/wallets/base-wallet/config";
10 | import { buildDummySignature } from "./signatures";
11 | import { hashConfig, KeystoreConfig } from "@/config";
12 |
13 |
14 | export type MakeCallsParameters = {
15 | account: Address;
16 | ownerIndex: bigint;
17 | calls: Call[];
18 | paymasterAndData?: Hex;
19 | initialConfigData?: Hex;
20 | privateKey: P256PrivateKey;
21 | }
22 |
23 | /**
24 | * Creates and sends a Base Wallet user operation signed with a WebAuthn/P256 private key.
25 | *
26 | * @param keystoreID - The hexadecimal ID of the keystore.
27 | * @param privateKey - The private key object used for signing.
28 | * @param calls - An array of calls to be executed.
29 | * @param paymasterData - Optional hexadecimal data for the paymaster. Defaults to "0x".
30 | * @returns A promise of the user operation hash.
31 | */
32 | export async function makeCalls({ account, ownerIndex, calls, privateKey, paymasterAndData, initialConfigData }: MakeCallsParameters) {
33 | initialConfigData ??= encodeConfigData(getConfigDataForPrivateKey(privateKey));
34 | const op = await buildUserOp({
35 | account,
36 | initialConfigData,
37 | calls,
38 | paymasterAndData: paymasterAndData ?? "0x",
39 | dummySignature: buildDummySignature(),
40 | provider: client,
41 | bundlerProvider: bundlerClient,
42 | });
43 |
44 | const hash = getUserOpHash({ userOperation: op, chainId: BigInt(chain.id) });
45 | op.signature = await signAndWrap({ hash, privateKey, ownerIndex });
46 |
47 | const opHash = await bundlerClient.sendUserOperation({
48 | userOperation: op,
49 | entryPoint: entryPointAddress,
50 | });
51 |
52 | console.log("opHash", opHash);
53 | }
54 |
55 | /**
56 | * Signs the authorization for a setConfig transaction.
57 | *
58 | * @param config - The new configuration data.
59 | * @param ownerIndex - The index of the owner.
60 | * @param privateKey - The private key object used for signing.
61 | * @returns A promise of the encoded authorization signature.
62 | */
63 | export async function signSetConfigAuth({
64 | config,
65 | ownerIndex,
66 | privateKey,
67 | }: {
68 | config: KeystoreConfig,
69 | ownerIndex: bigint,
70 | privateKey: P256PrivateKey,
71 | }) {
72 | const hash = hashConfig(config);
73 | const sigAuth = await signAndWrap({ hash, privateKey, ownerIndex });
74 | const sigUpdate = "0x";
75 | return encodeAbiParameters(
76 | [
77 | { type: "bytes", name: "sigAuth" },
78 | { type: "bytes", name: "sigUpdate" },
79 | ],
80 | [sigAuth, sigUpdate]
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/webauthn/config-data.ts:
--------------------------------------------------------------------------------
1 | import { Hex, toHex } from "viem";
2 | import { CoinbaseSmartWalletConfigData, encodeConfigData } from "../../config";
3 |
4 |
5 | /**
6 | * Encodes the given P256 private key as the raw bytes of the Base Wallet config data format.
7 | *
8 | * @param privateKey - The P256 private key object.
9 | * @returns The config data as a hex string.
10 | */
11 | export function getConfigDataForPrivateKey(privateKey: any): CoinbaseSmartWalletConfigData {
12 | const pk256 = serializePublicKeyFromPoint(privateKey.x, privateKey.y);
13 | return { owners: [toHex(pk256)] };
14 | }
15 |
16 | /**
17 | * Encodes the given public key as the raw bytes of the Base Wallet config data format.
18 | *
19 | * @param publicKey - The public key as a Uint8Array.
20 | * @returns The config data as a hex string.
21 | */
22 | export function getConfigDataForPublicKey(publicKey: Uint8Array): CoinbaseSmartWalletConfigData {
23 | return { owners: [toHex(publicKey)] };
24 | }
25 |
26 | /**
27 | * Serializes a P256 public key into the 64-byte array that Base Wallet expects.
28 | *
29 | * @param x - The x coordinate of the public key as a Uint8Array.
30 | * @param y - The y coordinate of the public key as a Uint8Array.
31 | * @returns The serialized public key as a Uint8Array.
32 | */
33 | export function serializePublicKeyFromPoint(x: Uint8Array, y: Uint8Array): Uint8Array {
34 | const keyspaceData = new Uint8Array(64);
35 | keyspaceData.set(x, 0);
36 | keyspaceData.set(y, 32);
37 | return keyspaceData;
38 | }
39 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/webauthn/sign.ts:
--------------------------------------------------------------------------------
1 | import { base64urlnopad } from "@scure/base";
2 | import {
3 | decodeAbiParameters,
4 | encodePacked,
5 | hexToBytes,
6 | sha256,
7 | stringToBytes,
8 | type Hex
9 | } from "viem";
10 | import { wrapSignature } from "../../user-op";
11 | import { encodeWebAuthnAuth, preventSignatureMalleability } from "./signatures";
12 | import { getConfigDataForPrivateKey } from "./config-data";
13 |
14 | export type P256PrivateKey = {
15 | x: Buffer;
16 | y: Buffer;
17 | sign: (message: string, format: string) => Buffer;
18 | };
19 |
20 | export const authenticatorData = "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000";
21 |
22 | /**
23 | * Creates a WebAuthn-formatted payload and signs it with the provided P256 private key.
24 | *
25 | * This is intended to simulate a WebAuthn signature from a browser, not for use by wallets.
26 | *
27 | * @param challenge - The challenge in hexadecimal format.
28 | * @param authenticatorData - The authenticator data in hexadecimal format.
29 | * @param p256PrivateKey - The P-256 private key used for signing.
30 | * @returns A WebAuthn-formatted signature.
31 | */
32 | export function p256WebAuthnSign(
33 | { challenge, authenticatorData, p256PrivateKey }: { challenge: Hex; authenticatorData: Hex; p256PrivateKey: any; }
34 | ) {
35 | const challengeBase64 = base64urlnopad.encode(hexToBytes(challenge));
36 | const clientDataJSON = `{"type":"webauthn.get","challenge":"${challengeBase64}","origin":"https://keys.coinbase.com"}`;
37 | const clientDataJSONHash = sha256(stringToBytes(clientDataJSON));
38 | const message = encodePacked(["bytes", "bytes32"], [authenticatorData, clientDataJSONHash]);
39 | const sig = p256PrivateKey.sign(Buffer.from(message.slice(2), "hex"), "hex");
40 | const [unsafeR, unsafeS] = decodeAbiParameters([{ type: "uint256" }, { type: "uint256" }], `0x${sig}` as Hex);
41 | const {r, s} = preventSignatureMalleability({r: unsafeR, s: unsafeS});
42 | return { r, s, clientDataJSON, authenticatorData };
43 | }
44 |
45 | export type SignAndWrapParameters = {
46 | hash: Hex;
47 | ownerIndex: bigint;
48 | privateKey: P256PrivateKey;
49 | }
50 |
51 | /**
52 | * Signs a hash with the provided private key and wraps the signature for use by the Base Wallet contracts.
53 | *
54 | * @param hash - The hash to be signed.
55 | * @param ownerIndex - The index of the owner in the Base Wallet.
56 | * @param privateKey - The P256 private key used for signing.
57 | * @returns A promise that resolves to the wrapped signature as a hex string.
58 | */
59 | export async function signAndWrap({ hash, ownerIndex, privateKey }: SignAndWrapParameters): Promise {
60 | const signature = await p256WebAuthnSign({
61 | challenge: hash,
62 | authenticatorData,
63 | p256PrivateKey: privateKey,
64 | });
65 |
66 | return wrapSignature(ownerIndex, encodeWebAuthnAuth(signature));
67 | }
68 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/signers/webauthn/signatures.ts:
--------------------------------------------------------------------------------
1 | import { base64urlnopad } from "@scure/base";
2 | import { Hex, bytesToBigInt, decodeAbiParameters, encodeAbiParameters, hexToBigInt, hexToBytes, stringToHex } from "viem";
3 | import { wrapSignature } from "../../user-op";
4 |
5 |
6 | export interface WebAuthnSignature {
7 | r: bigint;
8 | s: bigint;
9 | clientDataJSON: string;
10 | authenticatorData: string;
11 | }
12 |
13 | export const WebAuthnAuthStruct = {
14 | components: [
15 | {
16 | name: "authenticatorData",
17 | type: "bytes",
18 | },
19 | { name: "clientDataJSON", type: "bytes" },
20 | { name: "challengeIndex", type: "uint256" },
21 | { name: "typeIndex", type: "uint256" },
22 | {
23 | name: "r",
24 | type: "uint256",
25 | },
26 | {
27 | name: "s",
28 | type: "uint256",
29 | },
30 | ],
31 | name: "WebAuthnAuth",
32 | type: "tuple",
33 | };
34 |
35 | /**
36 | * Builds a dummy signature for estimating the gas cost of user operations.
37 | *
38 | * @returns {Uint8Array} The encoded dummy signature.
39 | */
40 | export function buildDummySignature(): Hex {
41 | const challenge = new Uint8Array(32);
42 | return wrapSignature(0n, encodeWebAuthnAuth({
43 | r: 0n,
44 | s: 0n,
45 | clientDataJSON: `{"type":"webauthn.get","challenge":"${base64urlnopad.encode(challenge)}","origin":"https://keys.coinbase.com"}`,
46 | authenticatorData: "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000",
47 | }));
48 | }
49 |
50 | /**
51 | * Encodes a WebAuthn signature into the WebAuthnAuth struct expected by the Base Wallet contracts.
52 | *
53 | * @param signature - The signature to encode.
54 | * @returns The encoded signature.
55 | */
56 | export function encodeWebAuthnAuth(
57 | { authenticatorData, clientDataJSON, r, s }: WebAuthnSignature
58 | ) {
59 | const challengeIndex = clientDataJSON.indexOf("\"challenge\":");
60 | const typeIndex = clientDataJSON.indexOf("\"type\":");
61 |
62 | return encodeAbiParameters(
63 | [WebAuthnAuthStruct],
64 | [
65 | {
66 | authenticatorData,
67 | clientDataJSON: stringToHex(clientDataJSON),
68 | challengeIndex,
69 | typeIndex,
70 | r,
71 | s,
72 | },
73 | ]
74 | );
75 | }
76 |
77 | /**
78 | * Ensures the signature is not malleable to pass the check in webauthn-sol.
79 | *
80 | * @returns The signature components.
81 | */
82 | export function preventSignatureMalleability({r, s}: { r: bigint; s: bigint; }): { r: bigint; s: bigint; } {
83 | const n = hexToBigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551");
84 | if (s > n / 2n) {
85 | s = n - s;
86 | }
87 | return { r, s };
88 | }
89 |
90 | /**
91 | * Decodes an ASN.1 sequence from a DER encoded signature.
92 | *
93 | * @param input - The DER encoded signature.
94 | * @returns The decoded sequence.
95 | */
96 | export function decodeASN1Sequence(input: Uint8Array): Uint8Array[] {
97 | if (input[0] !== 0x30) {
98 | throw new Error("Invalid ASN.1 sequence");
99 | }
100 | const seqLength = input[1];
101 | const values = [];
102 | let bytes = input.slice(2, 2 + seqLength);
103 | while (bytes.length > 0) {
104 | const tag = bytes[0];
105 | if (tag !== 0x02) {
106 | throw new Error("Invalid ASN.1 integersequence");
107 | }
108 | const valueLength = bytes[1];
109 | const value = bytes.slice(2, 2 + valueLength);
110 | values.push(value);
111 | bytes = bytes.slice(2 + valueLength);
112 | }
113 | return values;
114 | }
115 |
116 | /**
117 | * Converts a DER encoded signature to the format expected by the Base Wallet contracts.
118 | *
119 | * @param signature - The DER encoded signature.
120 | * @returns The converted signature.
121 | */
122 | export function convertDERSignature(signature: Hex): { r: bigint; s: bigint; } {
123 | const input = hexToBytes(signature);
124 | const values = decodeASN1Sequence(input);
125 | const r = bytesToBigInt(values[0]);
126 | const s = bytesToBigInt(values[1]);
127 | return preventSignatureMalleability({ r, s });
128 | }
129 |
--------------------------------------------------------------------------------
/src/wallets/base-wallet/user-op.ts:
--------------------------------------------------------------------------------
1 | import { estimateUserOperationGas, getRequiredPrefund, UserOperation } from "permissionless";
2 | import { Address, createPublicClient, custom, EIP1193Provider, encodeAbiParameters, encodeFunctionData, formatEther, fromHex, Hex, keccak256, PublicClient } from "viem";
3 | import * as chains from "viem/chains";
4 | import { estimateFeesPerGas, readContract } from "viem/actions";
5 | import { accountAbi, accountFactoryAbi, accountFactoryAddress, entryPointAbi, entryPointAddress } from "@generated";
6 | import { getIsDeployed } from "./contract";
7 | import { createCustomClient, ProviderClientConfig, EthereumProvider } from "@/client";
8 |
9 | export type BuildUserOpParameters = {
10 | account: Address;
11 | calls: Call[];
12 | initialConfigData: Hex;
13 | paymasterAndData: Hex;
14 | dummySignature: Hex;
15 | provider: ProviderClientConfig;
16 | bundlerProvider: ProviderClientConfig;
17 | }
18 |
19 | /**
20 | * Builds a UserOperation object for a given client and parameters.
21 | *
22 | * @param client - The PublicClient instance to interact with the blockchain.
23 | * @param initialConfigData - The initial configuration data for the wallet.
24 | * @param calls - An array of Call objects representing the operations to be performed.
25 | * @param paymasterAndData - The paymaster and data in hexadecimal format (default is "0x").
26 | * @param signatureType - The type of signature to use ("secp256k1" or "webauthn").
27 | * @returns A promise that resolves to a UserOperation object.
28 | * @throws Will throw an error if the sender's balance is less than the required prefund.
29 | */
30 | export async function buildUserOp({
31 | account,
32 | initialConfigData,
33 | calls,
34 | paymasterAndData = "0x",
35 | dummySignature,
36 | provider,
37 | bundlerProvider,
38 | }: BuildUserOpParameters): Promise {
39 | const client = createCustomClient(provider);
40 | const bundlerClient = createCustomClient(bundlerProvider);
41 | let initCode: Hex = "0x";
42 | if (!await getIsDeployed(client, account)) {
43 | const counterfactualAddress = await getAddress(client, { initialConfigData, nonce: 0n });
44 | if (account !== counterfactualAddress) {
45 | throw new Error(`Counterfactual address ${counterfactualAddress} does not match expected address ${account}. Did you provide the correct initial config data?`);
46 | }
47 |
48 | initCode = getInitCode({
49 | initialConfigData,
50 | nonce: 0n,
51 | provider: client,
52 | });
53 | }
54 | const callData = buildUserOperationCalldata({ calls });
55 | const nonce = await readContract(client, {
56 | address: entryPointAddress,
57 | abi: entryPointAbi,
58 | functionName: "getNonce",
59 | args: [account, 0n],
60 | });
61 | let maxFeesPerGas = await estimateFeesPerGas(client);
62 | // FIXME: Figure out what's wrong with the gas estimation so we don't have to pad the fees.
63 | maxFeesPerGas.maxFeePerGas += 1000000n
64 | maxFeesPerGas.maxPriorityFeePerGas += 1000000n
65 |
66 | const op = {
67 | sender: account,
68 | nonce,
69 | initCode,
70 | callData,
71 | paymasterAndData,
72 | signature: dummySignature,
73 | preVerificationGas: 5_000_000n,
74 | verificationGasLimit: 5_000_000n,
75 | callGasLimit: 5_000_000n,
76 | ...maxFeesPerGas,
77 | };
78 |
79 | const requiredPrefund = getRequiredPrefund({
80 | userOperation: op,
81 | });
82 | const senderBalance = await client.getBalance({
83 | address: account,
84 | });
85 | if (senderBalance < requiredPrefund) {
86 | throw new Error(`Sender address ${account} balance (${formatEther(senderBalance)} ETH) is less than required prefund (${formatEther(requiredPrefund)} ETH)`);
87 | }
88 |
89 | // NOTE: The gas limits provided in the user operation seem to override any
90 | // estimated limits, which makes this estimate redundant.
91 | const gasLimits = await estimateUserOperationGas(bundlerClient, {
92 | userOperation: op,
93 | entryPoint: entryPointAddress,
94 | });
95 |
96 | return {
97 | ...op,
98 | ...gasLimits,
99 | };
100 | }
101 |
102 | /**
103 | * Generates the initcode for a Base Wallet to include with its first user operation.
104 | *
105 | * @param initialConfigData - The initial configuration data for the wallet.
106 | * @param nonce - The nonce value used to deploy a unique wallet.
107 | * @returns The generated initialization code.
108 | */
109 | export function getInitCode({
110 | initialConfigData,
111 | nonce,
112 | provider,
113 | }: {
114 | initialConfigData: Hex;
115 | nonce: bigint;
116 | provider: ProviderClientConfig;
117 | }): Hex {
118 | const client = createCustomClient(provider);
119 | if (!client.chain?.id) {
120 | throw new Error("Chain not found");
121 | }
122 |
123 | return `${accountFactoryAddress[client.chain.id as keyof typeof accountFactoryAddress]}${
124 | createAccountCalldata({
125 | initialConfigData,
126 | nonce,
127 | }).slice(2)
128 | }`;
129 | }
130 |
131 | /**
132 | * Generates the calldata for creating a Base Wallet.
133 | *
134 | * @param initialConfigData - The initial configuration data for the wallet.
135 | * @param nonce - The nonce value used to deploy a unique wallet.
136 | * @returns The encoded function data for account creation.
137 | */
138 | export function createAccountCalldata({
139 | initialConfigData,
140 | nonce,
141 | }: {
142 | initialConfigData: Hex;
143 | nonce: bigint;
144 | }) {
145 | return encodeFunctionData({
146 | abi: accountFactoryAbi,
147 | functionName: "createAccount",
148 | args: [initialConfigData, nonce],
149 | });
150 | }
151 |
152 | /**
153 | * Retrieves the address of the Base Wallet with the provided initial configuration.
154 | *
155 | * @param client - The public client instance used to interact with the blockchain.
156 | * @param initialConfigData - The initial configuration data for the wallet.
157 | * @param nonce - The nonce value used to deploy a unique wallet.
158 | * @returns The Base Wallet address.
159 | */
160 | export async function getAddress(
161 | clientConfig: ProviderClientConfig,
162 | { initialConfigData, nonce }: { initialConfigData: Hex; nonce: bigint },
163 | ) {
164 | const client = createCustomClient(clientConfig);
165 |
166 | return await readContract(client, {
167 | abi: accountFactoryAbi,
168 | address: accountFactoryAddress[client.chain?.id as keyof typeof accountFactoryAddress],
169 | functionName: "getAddress",
170 | args: [initialConfigData, nonce],
171 | });
172 | }
173 |
174 | /**
175 | * Builds the calldata for one or more calls to be executed via executeBatch.
176 | *
177 | * @param calls - An array of calls to be made by the user operation.
178 | * @returns The encoded calldata for the user operation.
179 | */
180 | export function buildUserOperationCalldata({ calls }: { calls: Call[] }): Hex {
181 | // sort ascending order, 0 first
182 | const _calls = calls.sort((a, b) => a.index - b.index);
183 | return encodeFunctionData({
184 | abi: accountAbi,
185 | functionName: "executeBatch",
186 | args: [_calls],
187 | });
188 | }
189 |
190 | export type Call = {
191 | index: number;
192 | target: Address;
193 | value: bigint;
194 | data: Hex;
195 | };
196 |
197 | /**
198 | * Computes the hash of a user operation.
199 | *
200 | * @param userOperation - The user operation object containing various fields.
201 | * @param chainId - The chain ID that the user operation will be executed on.
202 | * @returns The computed hash of the user operation.
203 | */
204 | export function getUserOpHash({
205 | userOperation,
206 | chainId,
207 | }: {
208 | userOperation: UserOperation;
209 | chainId: bigint;
210 | }): Hex {
211 | const encodedUserOp = encodeAbiParameters(
212 | [
213 | { name: "sender", type: "address" },
214 | { name: "nonce", type: "uint256" },
215 | { name: "initCode", type: "bytes32" },
216 | { name: "callData", type: "bytes32" },
217 | { name: "callGasLimit", type: "uint256" },
218 | {
219 | name: "verificationGasLimit",
220 | type: "uint256",
221 | },
222 | {
223 | name: "preVerificationGas",
224 | type: "uint256",
225 | },
226 | { name: "maxFeePerGas", type: "uint256" },
227 | {
228 | name: "maxPriorityFeePerGas",
229 | type: "uint256",
230 | },
231 | { name: "paymasterAndData", type: "bytes32" },
232 | ],
233 | [
234 | userOperation.sender,
235 | userOperation.nonce,
236 | keccak256(userOperation.initCode),
237 | keccak256(userOperation.callData),
238 | userOperation.callGasLimit,
239 | userOperation.verificationGasLimit,
240 | userOperation.preVerificationGas,
241 | userOperation.maxFeePerGas,
242 | userOperation.maxPriorityFeePerGas,
243 | keccak256(userOperation.paymasterAndData),
244 | ],
245 | );
246 | const hashedUserOp = keccak256(encodedUserOp);
247 | const encodedWithChainAndEntryPoint = encodeAbiParameters(
248 | [
249 | { name: "userOpHash", type: "bytes32" },
250 | { name: "entryPoint", type: "address" },
251 | { name: "chainId", type: "uint256" },
252 | ],
253 | [hashedUserOp, entryPointAddress, chainId],
254 | );
255 | return keccak256(encodedWithChainAndEntryPoint);
256 | }
257 |
258 | /**
259 | * Wraps a signature with index of the owner that signed it.
260 | *
261 | * @param ownerIndex - The index of the owner.
262 | * @param signature - The signature to wrap.
263 | * @returns The wrapped signature.
264 | */
265 | export function wrapSignature(ownerIndex: bigint, signature: Hex): Hex {
266 | return encodeAbiParameters(
267 | [{
268 | components: [
269 | { name: "ownerIndex", type: "uint256" },
270 | { name: "signatureData", type: "bytes" },
271 | ],
272 | type: "tuple",
273 | }], [{
274 | ownerIndex,
275 | signatureData: signature,
276 | }]
277 | );
278 | }
279 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext"],
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "bundler",
7 | "moduleDetection": "force",
8 | "outDir": "./dist",
9 | "paths": {
10 | "@/*": ["./src/*"],
11 | "@generated": ["./generated.ts"],
12 | "@scripts/*": ["./scripts/*"]
13 | },
14 | "composite": true,
15 | "strict": true,
16 | "downlevelIteration": true,
17 | "skipLibCheck": true,
18 | "jsx": "react-jsx",
19 | "allowSyntheticDefaultImports": true,
20 | "forceConsistentCasingInFileNames": true,
21 | "allowJs": true,
22 | "types": [
23 | "bun-types" // add Bun global
24 | ]
25 | },
26 | "include": ["**/*.ts", "**/*.json"],
27 | "exclude": ["node_modules", "dist", "docs"]
28 | }
29 |
--------------------------------------------------------------------------------
/vocs.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vocs';
2 |
3 | export default defineConfig({
4 | title: 'Keyspace',
5 | description: 'Key and configuration management for cross-chain applications',
6 | topNav: [
7 | {
8 | text: 'Github',
9 | link: 'https://github.com/base-org/keyspace-client',
10 | },
11 | ],
12 | sidebar: [
13 | {
14 | text: 'Getting Started',
15 | link: '/',
16 | },
17 | {
18 | text: 'Using Keyspace',
19 | items: [
20 | {
21 | text: 'Keystore Basics',
22 | link: '/keystore-basics',
23 | },
24 | {
25 | text: 'Updating Your Keystore',
26 | link: '/updating-keystore',
27 | },
28 | {
29 | text: 'Using New Signers',
30 | link: '/using-new-signers',
31 | },
32 | {
33 | text: 'Revoking Signers',
34 | link: '/revoking-signers',
35 | },
36 | {
37 | text: 'Maintaining Wallets',
38 | link: '/maintaining-wallets',
39 | },
40 | ]
41 | },
42 | {
43 | text: 'Releases',
44 | link: '/releases',
45 | },
46 | {
47 | text: 'Roadmap',
48 | link: '/roadmap',
49 | },
50 | {
51 | text: 'References',
52 | link: '/references',
53 | },
54 | ],
55 | });
56 |
--------------------------------------------------------------------------------
/wagmi.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@wagmi/cli";
2 | import entrypointABI from "./abis/Entrypoint.json";
3 | import smartWalletABI from "./abis/SmartWallet.json";
4 | import smartWalletFactoryABI from "./abis/SmartWalletFactory.json";
5 | import anchorStateRegistryABI from "./abis/AnchorStateRegistry.json";
6 | import l1BlockABI from "./abis/L1Block.json";
7 | import { baseSepolia, optimismSepolia } from "viem/chains";
8 | import { Abi } from "viem";
9 |
10 | export default defineConfig({
11 | out: "./generated.ts",
12 | contracts: [
13 | {
14 | abi: smartWalletFactoryABI as Abi,
15 | address: {
16 | [baseSepolia.id]: "0x775062650652749c86686f68971F23Bb3FFf2b92",
17 | // [optimismSepolia.id]: "0x4Ca895d26b7eb26a9D980565732049d4199f32C8",
18 | },
19 | name: "AccountFactory",
20 | },
21 | {
22 | abi: smartWalletABI as Abi,
23 | name: "Account",
24 | },
25 | {
26 | abi: entrypointABI as Abi,
27 | name: "EntryPoint",
28 | address: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
29 | },
30 | {
31 | abi: anchorStateRegistryABI as Abi,
32 | name: "AnchorStateRegistry",
33 | address: "0x4C8BA32A5DAC2A720bb35CeDB51D6B067D104205",
34 | },
35 | {
36 | abi: l1BlockABI as Abi,
37 | name: "L1Block",
38 | address: "0x4200000000000000000000000000000000000015",
39 | }
40 | ],
41 | });
42 |
--------------------------------------------------------------------------------