@0xsequence/wallet-core 0.0.0-20250520201059

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +9 -0
  3. package/LICENSE +202 -0
  4. package/dist/envelope.d.ts +34 -0
  5. package/dist/envelope.d.ts.map +1 -0
  6. package/dist/envelope.js +96 -0
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +5 -0
  10. package/dist/relayer/index.d.ts +4 -0
  11. package/dist/relayer/index.d.ts.map +1 -0
  12. package/dist/relayer/index.js +3 -0
  13. package/dist/relayer/local.d.ts +28 -0
  14. package/dist/relayer/local.d.ts.map +1 -0
  15. package/dist/relayer/local.js +101 -0
  16. package/dist/relayer/pk-relayer.d.ts +18 -0
  17. package/dist/relayer/pk-relayer.d.ts.map +1 -0
  18. package/dist/relayer/pk-relayer.js +88 -0
  19. package/dist/relayer/relayer.d.ts +39 -0
  20. package/dist/relayer/relayer.d.ts.map +1 -0
  21. package/dist/relayer/relayer.js +1 -0
  22. package/dist/signers/index.d.ts +23 -0
  23. package/dist/signers/index.d.ts.map +1 -0
  24. package/dist/signers/index.js +10 -0
  25. package/dist/signers/passkey.d.ts +41 -0
  26. package/dist/signers/passkey.d.ts.map +1 -0
  27. package/dist/signers/passkey.js +196 -0
  28. package/dist/signers/pk/encrypted.d.ts +37 -0
  29. package/dist/signers/pk/encrypted.d.ts.map +1 -0
  30. package/dist/signers/pk/encrypted.js +123 -0
  31. package/dist/signers/pk/index.d.ts +35 -0
  32. package/dist/signers/pk/index.d.ts.map +1 -0
  33. package/dist/signers/pk/index.js +51 -0
  34. package/dist/signers/session/explicit.d.ts +18 -0
  35. package/dist/signers/session/explicit.d.ts.map +1 -0
  36. package/dist/signers/session/explicit.js +126 -0
  37. package/dist/signers/session/implicit.d.ts +20 -0
  38. package/dist/signers/session/implicit.d.ts.map +1 -0
  39. package/dist/signers/session/implicit.js +120 -0
  40. package/dist/signers/session/index.d.ts +4 -0
  41. package/dist/signers/session/index.d.ts.map +1 -0
  42. package/dist/signers/session/index.js +3 -0
  43. package/dist/signers/session/session.d.ts +11 -0
  44. package/dist/signers/session/session.d.ts.map +1 -0
  45. package/dist/signers/session/session.js +1 -0
  46. package/dist/signers/session-manager.d.ts +33 -0
  47. package/dist/signers/session-manager.d.ts.map +1 -0
  48. package/dist/signers/session-manager.js +181 -0
  49. package/dist/state/cached.d.ts +59 -0
  50. package/dist/state/cached.d.ts.map +1 -0
  51. package/dist/state/cached.js +157 -0
  52. package/dist/state/index.d.ts +61 -0
  53. package/dist/state/index.d.ts.map +1 -0
  54. package/dist/state/index.js +4 -0
  55. package/dist/state/local/index.d.ts +98 -0
  56. package/dist/state/local/index.d.ts.map +1 -0
  57. package/dist/state/local/index.js +247 -0
  58. package/dist/state/local/indexed-db.d.ts +41 -0
  59. package/dist/state/local/indexed-db.d.ts.map +1 -0
  60. package/dist/state/local/indexed-db.js +149 -0
  61. package/dist/state/local/memory.d.ts +41 -0
  62. package/dist/state/local/memory.d.ts.map +1 -0
  63. package/dist/state/local/memory.js +77 -0
  64. package/dist/state/remote/dev-http.d.ts +57 -0
  65. package/dist/state/remote/dev-http.d.ts.map +1 -0
  66. package/dist/state/remote/dev-http.js +162 -0
  67. package/dist/state/remote/index.d.ts +2 -0
  68. package/dist/state/remote/index.d.ts.map +1 -0
  69. package/dist/state/remote/index.js +1 -0
  70. package/dist/state/utils.d.ts +12 -0
  71. package/dist/state/utils.d.ts.map +1 -0
  72. package/dist/state/utils.js +29 -0
  73. package/dist/wallet.d.ts +58 -0
  74. package/dist/wallet.d.ts.map +1 -0
  75. package/dist/wallet.js +306 -0
  76. package/package.json +33 -0
  77. package/src/envelope.ts +148 -0
  78. package/src/index.ts +6 -0
  79. package/src/relayer/index.ts +3 -0
  80. package/src/relayer/local.ts +125 -0
  81. package/src/relayer/pk-relayer.ts +110 -0
  82. package/src/relayer/relayer.ts +52 -0
  83. package/src/signers/index.ts +44 -0
  84. package/src/signers/passkey.ts +284 -0
  85. package/src/signers/pk/encrypted.ts +153 -0
  86. package/src/signers/pk/index.ts +77 -0
  87. package/src/signers/session/explicit.ts +173 -0
  88. package/src/signers/session/implicit.ts +145 -0
  89. package/src/signers/session/index.ts +3 -0
  90. package/src/signers/session/session.ts +26 -0
  91. package/src/signers/session-manager.ts +241 -0
  92. package/src/state/cached.ts +233 -0
  93. package/src/state/index.ts +85 -0
  94. package/src/state/local/index.ts +422 -0
  95. package/src/state/local/indexed-db.ts +204 -0
  96. package/src/state/local/memory.ts +126 -0
  97. package/src/state/remote/dev-http.ts +253 -0
  98. package/src/state/remote/index.ts +1 -0
  99. package/src/state/utils.ts +50 -0
  100. package/src/wallet.ts +390 -0
  101. package/test/constants.ts +15 -0
  102. package/test/session-manager.test.ts +451 -0
  103. package/test/setup.ts +63 -0
  104. package/test/wallet.test.ts +90 -0
  105. package/tsconfig.json +10 -0
  106. package/vitest.config.ts +9 -0
package/src/wallet.ts ADDED
@@ -0,0 +1,390 @@
1
+ import {
2
+ Config,
3
+ Constants,
4
+ Context,
5
+ Erc6492,
6
+ Payload,
7
+ Address as SequenceAddress,
8
+ Signature as SequenceSignature,
9
+ } from '@0xsequence/wallet-primitives'
10
+ import { AbiFunction, Address, Bytes, Hex, Provider, TypedData } from 'ox'
11
+ import * as Envelope from './envelope.js'
12
+ import * as State from './state/index.js'
13
+
14
+ export type WalletOptions = {
15
+ context: Context.Context
16
+ stateProvider: State.Provider
17
+ guest: Address.Address
18
+ }
19
+
20
+ export const DefaultWalletOptions: WalletOptions = {
21
+ context: Context.Dev1,
22
+ stateProvider: new State.Local.Provider(),
23
+ guest: Constants.DefaultGuest,
24
+ }
25
+
26
+ export type WalletStatus = {
27
+ address: Address.Address
28
+ isDeployed: boolean
29
+ implementation?: Address.Address
30
+ stage?: 'stage1' | 'stage2'
31
+ configuration: Config.Config
32
+ imageHash: Hex.Hex
33
+ /** Pending updates in reverse chronological order (newest first) */
34
+ pendingUpdates: Array<{ imageHash: Hex.Hex; signature: SequenceSignature.RawSignature }>
35
+ chainId?: bigint
36
+ }
37
+
38
+ export type WalletStatusWithOnchain = WalletStatus & {
39
+ onChainImageHash: Hex.Hex
40
+ }
41
+
42
+ export class Wallet {
43
+ public readonly context: Context.Context
44
+ public readonly guest: Address.Address
45
+ public readonly stateProvider: State.Provider
46
+
47
+ constructor(
48
+ readonly address: Address.Address,
49
+ options?: Partial<WalletOptions>,
50
+ ) {
51
+ const combinedOptions = { ...DefaultWalletOptions, ...options }
52
+ this.context = combinedOptions.context
53
+ this.guest = combinedOptions.guest
54
+ this.stateProvider = combinedOptions.stateProvider
55
+ }
56
+
57
+ static async fromConfiguration(configuration: Config.Config, options?: Partial<WalletOptions>): Promise<Wallet> {
58
+ const merged = { ...DefaultWalletOptions, ...options }
59
+ //FIXME Validate configuration (weights not too large, total weights above threshold, etc)
60
+ await merged.stateProvider.saveWallet(configuration, merged.context)
61
+ return new Wallet(SequenceAddress.from(configuration, merged.context), merged)
62
+ }
63
+
64
+ async isDeployed(provider: Provider.Provider): Promise<boolean> {
65
+ return (await provider.request({ method: 'eth_getCode', params: [this.address, 'pending'] })) !== '0x'
66
+ }
67
+
68
+ async buildDeployTransaction(): Promise<{ to: Address.Address; data: Hex.Hex }> {
69
+ const deployInformation = await this.stateProvider.getDeploy(this.address)
70
+ if (!deployInformation) {
71
+ throw new Error(`cannot find deploy information for ${this.address}`)
72
+ }
73
+ return Erc6492.deploy(deployInformation.imageHash, deployInformation.context)
74
+ }
75
+
76
+ async prepareUpdate(configuration: Config.Config): Promise<Envelope.Envelope<Payload.ConfigUpdate>> {
77
+ const imageHash = Config.hashConfiguration(configuration)
78
+ const blankEvelope = (
79
+ await Promise.all([
80
+ this.prepareBlankEnvelope(0n),
81
+ // TODO: Add save configuration
82
+ this.stateProvider.saveWallet(configuration, this.context),
83
+ ])
84
+ )[0]
85
+
86
+ return {
87
+ ...blankEvelope,
88
+ payload: Payload.fromConfigUpdate(Bytes.toHex(imageHash)),
89
+ }
90
+ }
91
+
92
+ async submitUpdate(
93
+ envelope: Envelope.Signed<Payload.ConfigUpdate>,
94
+ options?: { validateSave?: boolean },
95
+ ): Promise<void> {
96
+ const [status, newConfig] = await Promise.all([
97
+ this.getStatus(),
98
+ this.stateProvider.getConfiguration(envelope.payload.imageHash),
99
+ ])
100
+
101
+ if (!newConfig) {
102
+ throw new Error(`cannot find configuration details for ${envelope.payload.imageHash}`)
103
+ }
104
+
105
+ // Verify the new configuration is valid
106
+ const updatedEnvelope = { ...envelope, configuration: status.configuration }
107
+ const { weight, threshold } = Envelope.weightOf(updatedEnvelope)
108
+ if (weight < threshold) {
109
+ throw new Error('insufficient weight in envelope')
110
+ }
111
+
112
+ const signature = Envelope.encodeSignature(updatedEnvelope)
113
+ await this.stateProvider.saveUpdate(this.address, newConfig, signature)
114
+
115
+ if (options?.validateSave) {
116
+ const status = await this.getStatus()
117
+ if (Hex.from(Config.hashConfiguration(status.configuration)) !== envelope.payload.imageHash) {
118
+ throw new Error('configuration not saved')
119
+ }
120
+ }
121
+ }
122
+ async getStatus<T extends Provider.Provider | undefined = undefined>(
123
+ provider?: T,
124
+ ): Promise<T extends Provider.Provider ? WalletStatusWithOnchain : WalletStatus> {
125
+ let isDeployed = false
126
+ let implementation: Address.Address | undefined
127
+ let stage: 'stage1' | 'stage2' | undefined
128
+ let chainId: bigint | undefined
129
+ let imageHash: Hex.Hex
130
+ let updates: Array<{ imageHash: Hex.Hex; signature: SequenceSignature.RawSignature }> = []
131
+ let onChainImageHash: Hex.Hex | undefined
132
+
133
+ if (provider) {
134
+ // Get chain ID, deployment status, and implementation
135
+ const requests = await Promise.all([
136
+ provider.request({ method: 'eth_chainId' }),
137
+ this.isDeployed(provider),
138
+ provider
139
+ .request({
140
+ method: 'eth_call',
141
+ params: [{ to: this.address, data: AbiFunction.encodeData(Constants.GET_IMPLEMENTATION) }],
142
+ })
143
+ .then((res) => {
144
+ const address = `0x${res.slice(-40)}`
145
+ Address.assert(address, { strict: false })
146
+ return address
147
+ })
148
+ .catch(() => undefined),
149
+ ])
150
+
151
+ chainId = BigInt(requests[0])
152
+ isDeployed = requests[1]
153
+ implementation = requests[2]
154
+
155
+ // Determine stage based on implementation address
156
+ if (implementation) {
157
+ if (Address.isEqual(implementation, this.context.stage1)) {
158
+ stage = 'stage1'
159
+ } else if (Address.isEqual(implementation, this.context.stage2)) {
160
+ stage = 'stage2'
161
+ }
162
+ }
163
+
164
+ // Get image hash and updates
165
+ if (isDeployed && stage === 'stage2') {
166
+ // For deployed stage2 wallets, get the image hash from the contract
167
+ onChainImageHash = await provider.request({
168
+ method: 'eth_call',
169
+ params: [{ to: this.address, data: AbiFunction.encodeData(Constants.IMAGE_HASH) }],
170
+ })
171
+ } else {
172
+ // For non-deployed or stage1 wallets, get the deploy hash
173
+ const deployInformation = await this.stateProvider.getDeploy(this.address)
174
+ if (!deployInformation) {
175
+ throw new Error(`cannot find deploy information for ${this.address}`)
176
+ }
177
+ onChainImageHash = deployInformation.imageHash
178
+ }
179
+
180
+ // Get configuration updates
181
+ updates = await this.stateProvider.getConfigurationUpdates(this.address, onChainImageHash)
182
+ imageHash = updates[updates.length - 1]?.imageHash ?? onChainImageHash
183
+ } else {
184
+ // Without a provider, we can only get information from the state provider
185
+ const deployInformation = await this.stateProvider.getDeploy(this.address)
186
+ if (!deployInformation) {
187
+ throw new Error(`cannot find deploy information for ${this.address}`)
188
+ }
189
+ updates = await this.stateProvider.getConfigurationUpdates(this.address, deployInformation.imageHash)
190
+ imageHash = updates[updates.length - 1]?.imageHash ?? deployInformation.imageHash
191
+ }
192
+
193
+ // Get the current configuration
194
+ const configuration = await this.stateProvider.getConfiguration(imageHash)
195
+ if (!configuration) {
196
+ throw new Error(`cannot find configuration details for ${this.address}`)
197
+ }
198
+
199
+ if (provider) {
200
+ return {
201
+ address: this.address,
202
+ isDeployed,
203
+ implementation,
204
+ stage,
205
+ configuration,
206
+ imageHash,
207
+ pendingUpdates: [...updates].reverse(),
208
+ chainId,
209
+ onChainImageHash: onChainImageHash!,
210
+ } as T extends Provider.Provider ? WalletStatusWithOnchain : WalletStatus
211
+ } else {
212
+ return {
213
+ address: this.address,
214
+ isDeployed,
215
+ implementation,
216
+ stage,
217
+ configuration,
218
+ imageHash,
219
+ pendingUpdates: [...updates].reverse(),
220
+ chainId,
221
+ } as T extends Provider.Provider ? WalletStatusWithOnchain : WalletStatus
222
+ }
223
+ }
224
+
225
+ async getNonce(provider: Provider.Provider, space: bigint): Promise<bigint> {
226
+ const result = await provider.request({
227
+ method: 'eth_call',
228
+ params: [{ to: this.address, data: AbiFunction.encodeData(Constants.READ_NONCE, [space]) }],
229
+ })
230
+
231
+ if (result === '0x' || result.length === 0) {
232
+ return 0n
233
+ }
234
+
235
+ return BigInt(result)
236
+ }
237
+
238
+ async prepareTransaction(
239
+ provider: Provider.Provider,
240
+ calls: Payload.Call[],
241
+ options?: { space?: bigint; noConfigUpdate?: boolean },
242
+ ): Promise<Envelope.Envelope<Payload.Calls>> {
243
+ const space = options?.space ?? 0n
244
+
245
+ const [chainId, nonce] = await Promise.all([
246
+ provider.request({ method: 'eth_chainId' }),
247
+ this.getNonce(provider, space),
248
+ ])
249
+
250
+ // If the latest configuration does not match the onchain configuration
251
+ // then we bundle the update into the transaction envelope
252
+ if (!options?.noConfigUpdate) {
253
+ const status = await this.getStatus(provider)
254
+ if (status.imageHash !== status.onChainImageHash) {
255
+ calls.push({
256
+ to: this.address,
257
+ value: 0n,
258
+ data: AbiFunction.encodeData(Constants.UPDATE_IMAGE_HASH, [status.imageHash]),
259
+ gasLimit: 0n,
260
+ delegateCall: false,
261
+ onlyFallback: false,
262
+ behaviorOnError: 'revert',
263
+ })
264
+ }
265
+ }
266
+
267
+ return {
268
+ payload: {
269
+ type: 'call',
270
+ space,
271
+ nonce,
272
+ calls,
273
+ },
274
+ ...(await this.prepareBlankEnvelope(BigInt(chainId))),
275
+ }
276
+ }
277
+
278
+ async buildTransaction(provider: Provider.Provider, envelope: Envelope.Signed<Payload.Calls>) {
279
+ const status = await this.getStatus(provider)
280
+
281
+ const updatedEnvelope = { ...envelope, configuration: status.configuration }
282
+ const { weight, threshold } = Envelope.weightOf(updatedEnvelope)
283
+ if (weight < threshold) {
284
+ throw new Error('insufficient weight in envelope')
285
+ }
286
+
287
+ const signature = Envelope.encodeSignature(updatedEnvelope)
288
+
289
+ if (status.isDeployed) {
290
+ return {
291
+ to: this.address,
292
+ data: AbiFunction.encodeData(Constants.EXECUTE, [
293
+ Bytes.toHex(Payload.encode(envelope.payload)),
294
+ Bytes.toHex(
295
+ SequenceSignature.encodeSignature({
296
+ ...signature,
297
+ suffix: status.pendingUpdates.map(({ signature }) => signature),
298
+ }),
299
+ ),
300
+ ]),
301
+ }
302
+ } else {
303
+ const deploy = await this.buildDeployTransaction()
304
+
305
+ return {
306
+ to: this.guest,
307
+ data: Bytes.toHex(
308
+ Payload.encode({
309
+ type: 'call',
310
+ space: 0n,
311
+ nonce: 0n,
312
+ calls: [
313
+ {
314
+ to: deploy.to,
315
+ value: 0n,
316
+ data: deploy.data,
317
+ gasLimit: 0n,
318
+ delegateCall: false,
319
+ onlyFallback: false,
320
+ behaviorOnError: 'revert',
321
+ },
322
+ {
323
+ to: this.address,
324
+ value: 0n,
325
+ data: AbiFunction.encodeData(Constants.EXECUTE, [
326
+ Bytes.toHex(Payload.encode(envelope.payload)),
327
+ Bytes.toHex(
328
+ SequenceSignature.encodeSignature({
329
+ ...signature,
330
+ suffix: status.pendingUpdates.map(({ signature }) => signature),
331
+ }),
332
+ ),
333
+ ]),
334
+ gasLimit: 0n,
335
+ delegateCall: false,
336
+ onlyFallback: false,
337
+ behaviorOnError: 'revert',
338
+ },
339
+ ],
340
+ }),
341
+ ),
342
+ }
343
+ }
344
+ }
345
+
346
+ async prepareMessageSignature(
347
+ message: string | Hex.Hex | Payload.TypedDataToSign,
348
+ chainId: bigint,
349
+ ): Promise<Envelope.Envelope<Payload.Message>> {
350
+ let encodedMessage: Hex.Hex
351
+ if (typeof message !== 'string') {
352
+ encodedMessage = TypedData.encode(message)
353
+ } else {
354
+ let hexMessage = Hex.validate(message) ? message : Hex.fromString(message)
355
+ const messageSize = Hex.size(hexMessage)
356
+ encodedMessage = Hex.concat(Hex.fromString(`${`\x19Ethereum Signed Message:\n${messageSize}`}`), hexMessage)
357
+ }
358
+ return {
359
+ ...(await this.prepareBlankEnvelope(chainId)),
360
+ payload: Payload.fromMessage(encodedMessage),
361
+ }
362
+ }
363
+
364
+ async buildMessageSignature(
365
+ envelope: Envelope.Signed<Payload.Message>,
366
+ provider?: Provider.Provider,
367
+ ): Promise<Bytes.Bytes> {
368
+ const status = await this.getStatus(provider)
369
+ const signature = Envelope.encodeSignature(envelope)
370
+ if (!status.isDeployed) {
371
+ const deployTransaction = await this.buildDeployTransaction()
372
+ signature.erc6492 = { to: deployTransaction.to, data: Bytes.fromHex(deployTransaction.data) }
373
+ }
374
+ const encoded = SequenceSignature.encodeSignature({
375
+ ...signature,
376
+ suffix: status.pendingUpdates.map(({ signature }) => signature),
377
+ })
378
+ return encoded
379
+ }
380
+
381
+ private async prepareBlankEnvelope(chainId: bigint) {
382
+ const status = await this.getStatus()
383
+
384
+ return {
385
+ wallet: this.address,
386
+ chainId: chainId,
387
+ configuration: status.configuration,
388
+ }
389
+ }
390
+ }
@@ -0,0 +1,15 @@
1
+ import { config as dotenvConfig } from 'dotenv'
2
+ import { Abi, AbiEvent, Address } from 'ox'
3
+
4
+ const envFile = process.env.CI ? '.env.test' : '.env.test.local'
5
+ dotenvConfig({ path: envFile })
6
+
7
+ export const EMITTER_ADDRESS: Address.Address = '0x7F6e420Ed3017A36bE6e1DA8e3AFE61569eb4840'
8
+ export const EMITTER_FUNCTIONS = Abi.from(['function explicitEmit()', 'function implicitEmit()'])
9
+ export const EMITTER_EVENT_TOPICS = [
10
+ AbiEvent.encode(AbiEvent.from('event Explicit(address sender)')).topics[0],
11
+ AbiEvent.encode(AbiEvent.from('event Implicit(address sender)')).topics[0],
12
+ ]
13
+
14
+ // Environment variables
15
+ export const LOCAL_RPC_URL = process.env.LOCAL_RPC_URL || 'http://localhost:8545'