@0xsequence/wallet-core 3.0.0-beta.9 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +290 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +158 -0
- package/dist/bundler/bundlers/pimlico.d.ts +3 -2
- package/dist/bundler/bundlers/pimlico.d.ts.map +1 -1
- package/dist/bundler/bundlers/pimlico.js +9 -3
- package/dist/env.d.ts +22 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/signers/index.d.ts +1 -1
- package/dist/signers/passkey.d.ts +11 -3
- package/dist/signers/passkey.d.ts.map +1 -1
- package/dist/signers/passkey.js +12 -6
- package/dist/signers/pk/encrypted.d.ts +13 -2
- package/dist/signers/pk/encrypted.d.ts.map +1 -1
- package/dist/signers/pk/encrypted.js +94 -15
- package/dist/signers/pk/index.d.ts +1 -1
- package/dist/signers/session/explicit.d.ts.map +1 -1
- package/dist/signers/session/explicit.js +5 -8
- package/dist/signers/session/implicit.d.ts.map +1 -1
- package/dist/signers/session/implicit.js +1 -1
- package/dist/signers/session/session.d.ts +1 -0
- package/dist/signers/session/session.d.ts.map +1 -1
- package/dist/signers/session/session.js +7 -0
- package/dist/signers/session-manager.d.ts +4 -0
- package/dist/signers/session-manager.d.ts.map +1 -1
- package/dist/signers/session-manager.js +65 -22
- package/dist/state/local/index.d.ts.map +1 -1
- package/dist/state/local/index.js +3 -2
- package/dist/state/local/indexed-db.d.ts +4 -1
- package/dist/state/local/indexed-db.d.ts.map +1 -1
- package/dist/state/local/indexed-db.js +12 -2
- package/dist/state/remote/dev-http.d.ts +2 -1
- package/dist/state/remote/dev-http.d.ts.map +1 -1
- package/dist/state/remote/dev-http.js +11 -5
- package/dist/state/sequence/index.d.ts +2 -1
- package/dist/state/sequence/index.d.ts.map +1 -1
- package/dist/state/sequence/index.js +14 -5
- package/dist/wallet.js +2 -2
- package/eslint.config.js +12 -0
- package/package.json +12 -10
- package/src/bundler/bundlers/pimlico.ts +10 -4
- package/src/env.ts +68 -0
- package/src/index.ts +1 -0
- package/src/signers/index.ts +1 -1
- package/src/signers/passkey.ts +21 -5
- package/src/signers/pk/encrypted.ts +103 -14
- package/src/signers/pk/index.ts +1 -1
- package/src/signers/session/explicit.ts +5 -20
- package/src/signers/session/implicit.ts +1 -2
- package/src/signers/session/session.ts +10 -2
- package/src/signers/session-manager.ts +82 -25
- package/src/state/local/index.ts +4 -2
- package/src/state/local/indexed-db.ts +15 -2
- package/src/state/remote/dev-http.ts +11 -5
- package/src/state/sequence/index.ts +15 -6
- package/src/wallet.ts +2 -2
- package/test/constants.ts +4 -0
- package/test/envelope.test.ts +0 -1
- package/test/session-manager.test.ts +345 -3
- package/test/signers-pk.test.ts +1 -1
- package/test/signers-session-implicit.test.ts +0 -2
- package/test/state/debug.test.ts +2 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hex, Address, PublicKey, Secp256k1, Bytes } from 'ox'
|
|
2
|
+
import { resolveCoreEnv, type CoreEnv, type CryptoLike, type StorageLike, type TextEncodingLike } from '../../env.js'
|
|
2
3
|
import { PkStore } from './index.js'
|
|
3
4
|
|
|
4
5
|
export interface EncryptedData {
|
|
@@ -17,6 +18,7 @@ export class EncryptedPksDb {
|
|
|
17
18
|
constructor(
|
|
18
19
|
private readonly localStorageKeyPrefix: string = 'e_pk_key_',
|
|
19
20
|
tableName: string = 'e_pk',
|
|
21
|
+
private readonly env?: CoreEnv,
|
|
20
22
|
) {
|
|
21
23
|
this.tableName = tableName
|
|
22
24
|
}
|
|
@@ -25,9 +27,59 @@ export class EncryptedPksDb {
|
|
|
25
27
|
return `pk_${address.toLowerCase()}`
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
private getIndexedDB(): IDBFactory {
|
|
31
|
+
const globalObj = globalThis as any
|
|
32
|
+
const indexedDb = this.env?.indexedDB ?? globalObj.indexedDB ?? globalObj.window?.indexedDB
|
|
33
|
+
if (!indexedDb) {
|
|
34
|
+
throw new Error('indexedDB is not available')
|
|
35
|
+
}
|
|
36
|
+
return indexedDb
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private getStorage(): StorageLike {
|
|
40
|
+
const storage = resolveCoreEnv(this.env).storage
|
|
41
|
+
if (!storage) {
|
|
42
|
+
throw new Error('storage is not available')
|
|
43
|
+
}
|
|
44
|
+
return storage
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getCrypto(): CryptoLike {
|
|
48
|
+
const globalObj = globalThis as any
|
|
49
|
+
const crypto = this.env?.crypto ?? globalObj.crypto ?? globalObj.window?.crypto
|
|
50
|
+
if (!crypto?.subtle || !crypto?.getRandomValues) {
|
|
51
|
+
throw new Error('crypto.subtle is not available')
|
|
52
|
+
}
|
|
53
|
+
return crypto
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private getTextEncoderCtor(): TextEncodingLike['TextEncoder'] {
|
|
57
|
+
const globalObj = globalThis as any
|
|
58
|
+
if (this.env?.text && (!this.env.text.TextEncoder || !this.env.text.TextDecoder)) {
|
|
59
|
+
throw new Error('env.text must provide both TextEncoder and TextDecoder')
|
|
60
|
+
}
|
|
61
|
+
const encoderCtor = this.env?.text?.TextEncoder ?? globalObj.TextEncoder ?? globalObj.window?.TextEncoder
|
|
62
|
+
if (!encoderCtor) {
|
|
63
|
+
throw new Error('TextEncoder is not available')
|
|
64
|
+
}
|
|
65
|
+
return encoderCtor
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private getTextDecoderCtor(): TextEncodingLike['TextDecoder'] {
|
|
69
|
+
const globalObj = globalThis as any
|
|
70
|
+
if (this.env?.text && (!this.env.text.TextEncoder || !this.env.text.TextDecoder)) {
|
|
71
|
+
throw new Error('env.text must provide both TextEncoder and TextDecoder')
|
|
72
|
+
}
|
|
73
|
+
const decoderCtor = this.env?.text?.TextDecoder ?? globalObj.TextDecoder ?? globalObj.window?.TextDecoder
|
|
74
|
+
if (!decoderCtor) {
|
|
75
|
+
throw new Error('TextDecoder is not available')
|
|
76
|
+
}
|
|
77
|
+
return decoderCtor
|
|
78
|
+
}
|
|
79
|
+
|
|
28
80
|
private openDB(): Promise<IDBDatabase> {
|
|
29
81
|
return new Promise((resolve, reject) => {
|
|
30
|
-
const request =
|
|
82
|
+
const request = this.getIndexedDB().open(this.dbName, this.dbVersion)
|
|
31
83
|
request.onupgradeneeded = () => {
|
|
32
84
|
const db = request.result
|
|
33
85
|
if (!db.objectStoreNames.contains(this.tableName)) {
|
|
@@ -73,7 +125,11 @@ export class EncryptedPksDb {
|
|
|
73
125
|
}
|
|
74
126
|
|
|
75
127
|
async generateAndStore(): Promise<EncryptedData> {
|
|
76
|
-
const
|
|
128
|
+
const crypto = this.getCrypto()
|
|
129
|
+
const storage = this.getStorage()
|
|
130
|
+
const TextEncoderCtor = this.getTextEncoderCtor()
|
|
131
|
+
|
|
132
|
+
const encryptionKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
|
|
77
133
|
'encrypt',
|
|
78
134
|
'decrypt',
|
|
79
135
|
])
|
|
@@ -84,13 +140,13 @@ export class EncryptedPksDb {
|
|
|
84
140
|
const address = Address.fromPublicKey(publicKey)
|
|
85
141
|
const keyPointer = this.localStorageKeyPrefix + address
|
|
86
142
|
|
|
87
|
-
const exportedKey = await
|
|
88
|
-
|
|
143
|
+
const exportedKey = await crypto.subtle.exportKey('jwk', encryptionKey)
|
|
144
|
+
storage.setItem(keyPointer, JSON.stringify(exportedKey))
|
|
89
145
|
|
|
90
|
-
const encoder = new
|
|
146
|
+
const encoder = new TextEncoderCtor()
|
|
91
147
|
const encodedPk = encoder.encode(privateKey)
|
|
92
|
-
const iv =
|
|
93
|
-
const encryptedBuffer = await
|
|
148
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
149
|
+
const encryptedBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, encryptionKey, encodedPk)
|
|
94
150
|
|
|
95
151
|
const encrypted: EncryptedData = {
|
|
96
152
|
iv,
|
|
@@ -113,7 +169,7 @@ export class EncryptedPksDb {
|
|
|
113
169
|
async getEncryptedPkStore(address: Address.Address): Promise<EncryptedPkStore | undefined> {
|
|
114
170
|
const entry = await this.getEncryptedEntry(address)
|
|
115
171
|
if (!entry) return
|
|
116
|
-
return new EncryptedPkStore(entry)
|
|
172
|
+
return new EncryptedPkStore(entry, this.env)
|
|
117
173
|
}
|
|
118
174
|
|
|
119
175
|
async listAddresses(): Promise<Address.Address[]> {
|
|
@@ -125,12 +181,41 @@ export class EncryptedPksDb {
|
|
|
125
181
|
const dbKey = this.computeDbKey(address)
|
|
126
182
|
await this.putData(dbKey, undefined)
|
|
127
183
|
const keyPointer = this.localStorageKeyPrefix + address
|
|
128
|
-
|
|
184
|
+
this.getStorage().removeItem(keyPointer)
|
|
129
185
|
}
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
export class EncryptedPkStore implements PkStore {
|
|
133
|
-
constructor(
|
|
189
|
+
constructor(
|
|
190
|
+
private readonly encrypted: EncryptedData,
|
|
191
|
+
private readonly env?: CoreEnv,
|
|
192
|
+
) {}
|
|
193
|
+
|
|
194
|
+
private getStorage(): StorageLike {
|
|
195
|
+
const storage = resolveCoreEnv(this.env).storage
|
|
196
|
+
if (!storage) {
|
|
197
|
+
throw new Error('storage is not available')
|
|
198
|
+
}
|
|
199
|
+
return storage
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private getCrypto(): CryptoLike {
|
|
203
|
+
const globalObj = globalThis as any
|
|
204
|
+
const crypto = this.env?.crypto ?? globalObj.crypto ?? globalObj.window?.crypto
|
|
205
|
+
if (!crypto?.subtle) {
|
|
206
|
+
throw new Error('crypto.subtle is not available')
|
|
207
|
+
}
|
|
208
|
+
return crypto
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private getTextDecoderCtor(): TextEncodingLike['TextDecoder'] {
|
|
212
|
+
const globalObj = globalThis as any
|
|
213
|
+
const decoderCtor = this.env?.text?.TextDecoder ?? globalObj.TextDecoder ?? globalObj.window?.TextDecoder
|
|
214
|
+
if (!decoderCtor) {
|
|
215
|
+
throw new Error('TextDecoder is not available')
|
|
216
|
+
}
|
|
217
|
+
return decoderCtor
|
|
218
|
+
}
|
|
134
219
|
|
|
135
220
|
address(): Address.Address {
|
|
136
221
|
return this.encrypted.address
|
|
@@ -141,16 +226,20 @@ export class EncryptedPkStore implements PkStore {
|
|
|
141
226
|
}
|
|
142
227
|
|
|
143
228
|
async signDigest(digest: Bytes.Bytes): Promise<{ r: bigint; s: bigint; yParity: number }> {
|
|
144
|
-
const
|
|
229
|
+
const storage = this.getStorage()
|
|
230
|
+
const crypto = this.getCrypto()
|
|
231
|
+
const TextDecoderCtor = this.getTextDecoderCtor()
|
|
232
|
+
|
|
233
|
+
const keyJson = storage.getItem(this.encrypted.keyPointer)
|
|
145
234
|
if (!keyJson) throw new Error('Encryption key not found in localStorage')
|
|
146
235
|
const jwk = JSON.parse(keyJson)
|
|
147
|
-
const encryptionKey = await
|
|
148
|
-
const decryptedBuffer = await
|
|
236
|
+
const encryptionKey = await crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, false, ['decrypt'])
|
|
237
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
149
238
|
{ name: 'AES-GCM', iv: this.encrypted.iv },
|
|
150
239
|
encryptionKey,
|
|
151
240
|
this.encrypted.data,
|
|
152
241
|
)
|
|
153
|
-
const decoder = new
|
|
242
|
+
const decoder = new TextDecoderCtor()
|
|
154
243
|
const privateKey = decoder.decode(decryptedBuffer) as Hex.Hex
|
|
155
244
|
return Secp256k1.sign({ payload: digest, privateKey })
|
|
156
245
|
}
|
package/src/signers/pk/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export class Pk implements SignerInterface, Witnessable {
|
|
|
52
52
|
return { ...signature, type: 'hash' }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async witness(stateWriter: State.Writer, wallet: Address.Address, extra?:
|
|
55
|
+
async witness(stateWriter: State.Writer, wallet: Address.Address, extra?: object): Promise<void> {
|
|
56
56
|
const payload = Payload.fromMessage(
|
|
57
57
|
Hex.fromString(
|
|
58
58
|
JSON.stringify({
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Constants,
|
|
3
|
-
Extensions,
|
|
4
|
-
Payload,
|
|
5
|
-
Permission,
|
|
6
|
-
SessionConfig,
|
|
7
|
-
SessionSignature,
|
|
8
|
-
} from '@0xsequence/wallet-primitives'
|
|
1
|
+
import { Constants, Payload, Permission, SessionConfig, SessionSignature } from '@0xsequence/wallet-primitives'
|
|
9
2
|
import { AbiFunction, AbiParameters, Address, Bytes, Hash, Hex, Provider } from 'ox'
|
|
10
3
|
import { MemoryPkStore, PkStore } from '../pk/index.js'
|
|
11
|
-
import { ExplicitSessionSigner, SessionSignerValidity, UsageLimit } from './session.js'
|
|
4
|
+
import { ExplicitSessionSigner, isIncrementCall, SessionSignerValidity, UsageLimit } from './session.js'
|
|
12
5
|
|
|
13
6
|
export type ExplicitParams = Omit<Permission.SessionPermissions, 'signer'>
|
|
14
7
|
|
|
@@ -215,11 +208,7 @@ export class Explicit implements ExplicitSessionSigner {
|
|
|
215
208
|
sessionManagerAddress: Address.Address,
|
|
216
209
|
provider?: Provider.Provider,
|
|
217
210
|
): Promise<boolean> {
|
|
218
|
-
if (
|
|
219
|
-
Address.isEqual(call.to, sessionManagerAddress) &&
|
|
220
|
-
Hex.size(call.data) > 4 &&
|
|
221
|
-
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
|
|
222
|
-
) {
|
|
211
|
+
if (isIncrementCall(call, sessionManagerAddress)) {
|
|
223
212
|
// Can sign increment usage calls
|
|
224
213
|
return true
|
|
225
214
|
}
|
|
@@ -241,11 +230,7 @@ export class Explicit implements ExplicitSessionSigner {
|
|
|
241
230
|
): Promise<SessionSignature.SessionCallSignature> {
|
|
242
231
|
const call = payload.calls[callIdx]!
|
|
243
232
|
let permissionIndex: number
|
|
244
|
-
if (
|
|
245
|
-
Address.isEqual(call.to, sessionManagerAddress) &&
|
|
246
|
-
Hex.size(call.data) > 4 &&
|
|
247
|
-
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
|
|
248
|
-
) {
|
|
233
|
+
if (isIncrementCall(call, sessionManagerAddress)) {
|
|
249
234
|
// Permission check not required. Use the first permission
|
|
250
235
|
permissionIndex = 0
|
|
251
236
|
} else {
|
|
@@ -323,7 +308,7 @@ export class Explicit implements ExplicitSessionSigner {
|
|
|
323
308
|
Bytes.fromHex(call.data).slice(Number(rule.offset), Number(rule.offset) + 32),
|
|
324
309
|
32,
|
|
325
310
|
)
|
|
326
|
-
|
|
311
|
+
const value: Bytes.Bytes = callDataValue.map((b, i) => b & rule.mask[i]!)
|
|
327
312
|
if (Bytes.toBigInt(value) === 0n) continue
|
|
328
313
|
|
|
329
314
|
// Add to list
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Attestation,
|
|
3
|
-
Extensions,
|
|
4
3
|
Payload,
|
|
5
4
|
Signature as SequenceSignature,
|
|
6
5
|
SessionConfig,
|
|
@@ -96,7 +95,7 @@ export class Implicit implements ImplicitSessionSigner {
|
|
|
96
95
|
)
|
|
97
96
|
const expectedResult = Bytes.toHex(Attestation.generateImplicitRequestMagic(this._attestation, wallet))
|
|
98
97
|
return acceptImplicitRequest === expectedResult
|
|
99
|
-
} catch
|
|
98
|
+
} catch {
|
|
100
99
|
// console.log('implicit signer unsupported call', call, error)
|
|
101
100
|
return false
|
|
102
101
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Payload, SessionConfig, SessionSignature } from '@0xsequence/wallet-primitives'
|
|
2
|
-
import { Address, Hex, Provider } from 'ox'
|
|
1
|
+
import { Constants, Payload, SessionConfig, SessionSignature } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { AbiFunction, Address, Hex, Provider } from 'ox'
|
|
3
3
|
|
|
4
4
|
export type SessionSignerInvalidReason =
|
|
5
5
|
| 'Expired'
|
|
@@ -68,3 +68,11 @@ export function isExplicitSessionSigner(signer: SessionSigner): signer is Explic
|
|
|
68
68
|
export function isImplicitSessionSigner(signer: SessionSigner): signer is ImplicitSessionSigner {
|
|
69
69
|
return 'identitySigner' in signer
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
export function isIncrementCall(call: Payload.Call, sessionManagerAddress: Address.Address): boolean {
|
|
73
|
+
return (
|
|
74
|
+
Address.isEqual(call.to, sessionManagerAddress) &&
|
|
75
|
+
Hex.size(call.data) >= 4 &&
|
|
76
|
+
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SessionSigner,
|
|
19
19
|
SessionSignerInvalidReason,
|
|
20
20
|
isImplicitSessionSigner,
|
|
21
|
+
isIncrementCall,
|
|
21
22
|
UsageLimit,
|
|
22
23
|
} from './session/index.js'
|
|
23
24
|
|
|
@@ -130,21 +131,16 @@ export class SessionManager implements SapientSigner {
|
|
|
130
131
|
}))
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (availableSigners.length === 0) {
|
|
144
|
-
throw new Error('No signers match the topology')
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Find supported signers for each call
|
|
134
|
+
/**
|
|
135
|
+
* Find one signer per call from the given candidate list (first that supports each call).
|
|
136
|
+
*/
|
|
137
|
+
private async findSignersForCallsWithCandidates(
|
|
138
|
+
wallet: Address.Address,
|
|
139
|
+
chainId: number,
|
|
140
|
+
calls: Payload.Call[],
|
|
141
|
+
topology: SessionConfig.SessionsTopology,
|
|
142
|
+
availableSigners: SessionSigner[],
|
|
143
|
+
): Promise<SessionSigner[]> {
|
|
148
144
|
const signers: SessionSigner[] = []
|
|
149
145
|
for (const call of calls) {
|
|
150
146
|
let supported = false
|
|
@@ -173,9 +169,67 @@ export class SessionManager implements SapientSigner {
|
|
|
173
169
|
if (expiredSupportedSigner) {
|
|
174
170
|
throw new Error(`Signer supporting call is expired: ${expiredSupportedSigner.address}`)
|
|
175
171
|
}
|
|
176
|
-
throw new Error(
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
throw new Error(`No signer supported for call. Call: to=${call.to}, data=${call.data}, value=${call.value}, `)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return signers
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async findSignersForCalls(wallet: Address.Address, chainId: number, calls: Payload.Call[]): Promise<SessionSigner[]> {
|
|
179
|
+
const topology = await this.topology
|
|
180
|
+
const identitySigners = SessionConfig.getIdentitySigners(topology)
|
|
181
|
+
if (identitySigners.length === 0) {
|
|
182
|
+
throw new Error('Identity signers not found')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const availableSigners = [...this._implicitSigners, ...this._explicitSigners]
|
|
186
|
+
if (availableSigners.length === 0) {
|
|
187
|
+
throw new Error('No signers match the topology')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const nonIncrementCalls: Payload.Call[] = []
|
|
191
|
+
const incrementCalls: Payload.Call[] = []
|
|
192
|
+
for (const call of calls) {
|
|
193
|
+
if (isIncrementCall(call, this.address)) {
|
|
194
|
+
incrementCalls.push(call)
|
|
195
|
+
} else {
|
|
196
|
+
nonIncrementCalls.push(call)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Find signers for non-increment calls
|
|
201
|
+
const nonIncrementSigners =
|
|
202
|
+
nonIncrementCalls.length > 0
|
|
203
|
+
? await this.findSignersForCallsWithCandidates(wallet, chainId, nonIncrementCalls, topology, availableSigners)
|
|
204
|
+
: []
|
|
205
|
+
|
|
206
|
+
let incrementSigners: SessionSigner[] = []
|
|
207
|
+
if (incrementCalls.length > 0) {
|
|
208
|
+
// Find signers for increment calls, preferring signers that signed non-increment calls
|
|
209
|
+
const incrementCandidates = [
|
|
210
|
+
...nonIncrementSigners,
|
|
211
|
+
...availableSigners.filter((s) => !nonIncrementSigners.includes(s)),
|
|
212
|
+
]
|
|
213
|
+
incrementSigners = await this.findSignersForCallsWithCandidates(
|
|
214
|
+
wallet,
|
|
215
|
+
chainId,
|
|
216
|
+
incrementCalls,
|
|
217
|
+
topology,
|
|
218
|
+
incrementCandidates,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Merge back in original call order
|
|
223
|
+
const signers: SessionSigner[] = []
|
|
224
|
+
let nonIncrementIndex = 0
|
|
225
|
+
let incrementIndex = 0
|
|
226
|
+
for (const call of calls) {
|
|
227
|
+
if (isIncrementCall(call, this.address)) {
|
|
228
|
+
signers.push(incrementSigners[incrementIndex]!)
|
|
229
|
+
incrementIndex++
|
|
230
|
+
} else {
|
|
231
|
+
signers.push(nonIncrementSigners[nonIncrementIndex]!)
|
|
232
|
+
nonIncrementIndex++
|
|
179
233
|
}
|
|
180
234
|
}
|
|
181
235
|
return signers
|
|
@@ -191,20 +245,23 @@ export class SessionManager implements SapientSigner {
|
|
|
191
245
|
}
|
|
192
246
|
const signers = await this.findSignersForCalls(wallet, chainId, calls)
|
|
193
247
|
|
|
194
|
-
//
|
|
195
|
-
const
|
|
248
|
+
// Map each signer to only their non-increment calls
|
|
249
|
+
const signerToNonIncrementCalls = new Map<SessionSigner, Payload.Call[]>()
|
|
196
250
|
signers.forEach((signer, index) => {
|
|
197
251
|
const call = calls[index]!
|
|
198
|
-
|
|
199
|
-
|
|
252
|
+
if (isIncrementCall(call, this.address)) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
const existing = signerToNonIncrementCalls.get(signer) || []
|
|
256
|
+
signerToNonIncrementCalls.set(signer, [...existing, call])
|
|
200
257
|
})
|
|
201
258
|
|
|
202
|
-
// Prepare increments for each explicit signer
|
|
259
|
+
// Prepare increments for each explicit signer from their non-increment calls only
|
|
203
260
|
const increments: UsageLimit[] = (
|
|
204
261
|
await Promise.all(
|
|
205
|
-
Array.from(
|
|
262
|
+
Array.from(signerToNonIncrementCalls.entries()).map(async ([signer, nonIncrementCalls]) => {
|
|
206
263
|
if (isExplicitSessionSigner(signer)) {
|
|
207
|
-
return signer.prepareIncrements(wallet, chainId,
|
|
264
|
+
return signer.prepareIncrements(wallet, chainId, nonIncrementCalls, this.address, this._provider!)
|
|
208
265
|
}
|
|
209
266
|
return []
|
|
210
267
|
}),
|
package/src/state/local/index.ts
CHANGED
|
@@ -203,7 +203,7 @@ export class Provider implements ProviderInterface {
|
|
|
203
203
|
fromImageHash: Hex.Hex,
|
|
204
204
|
options?: { allUpdates?: boolean },
|
|
205
205
|
): Promise<{ imageHash: Hex.Hex; signature: Signature.RawSignature }[]> {
|
|
206
|
-
|
|
206
|
+
const fromConfig = await this.store.loadConfig(fromImageHash)
|
|
207
207
|
if (!fromConfig) {
|
|
208
208
|
return []
|
|
209
209
|
}
|
|
@@ -384,7 +384,7 @@ export class Provider implements ProviderInterface {
|
|
|
384
384
|
|
|
385
385
|
if (Signature.isSignatureOfSapientSignerLeaf(topology.signature)) {
|
|
386
386
|
switch (topology.signature.address.toLowerCase()) {
|
|
387
|
-
case this.extensions.passkeys.toLowerCase():
|
|
387
|
+
case this.extensions.passkeys.toLowerCase(): {
|
|
388
388
|
const decoded = Extensions.Passkeys.decode(Bytes.fromHex(topology.signature.data))
|
|
389
389
|
|
|
390
390
|
if (!Extensions.Passkeys.isValidSignature(subdigest, decoded)) {
|
|
@@ -397,6 +397,8 @@ export class Provider implements ProviderInterface {
|
|
|
397
397
|
Extensions.Passkeys.rootFor(decoded.publicKey),
|
|
398
398
|
topology.signature,
|
|
399
399
|
)
|
|
400
|
+
}
|
|
401
|
+
|
|
400
402
|
default:
|
|
401
403
|
throw new Error(`Unsupported sapient signer: ${topology.signature.address}`)
|
|
402
404
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context, Payload, Signature, Config, GenericTree } from '@0xsequence/wallet-primitives'
|
|
2
2
|
import { Address, Hex } from 'ox'
|
|
3
|
+
import type { CoreEnv } from '../../env.js'
|
|
3
4
|
import { Store } from './index.js'
|
|
4
5
|
|
|
5
6
|
const DB_VERSION = 1
|
|
@@ -16,15 +17,27 @@ export class IndexedDbStore implements Store {
|
|
|
16
17
|
private _db: IDBDatabase | null = null
|
|
17
18
|
private dbName: string
|
|
18
19
|
|
|
19
|
-
constructor(
|
|
20
|
+
constructor(
|
|
21
|
+
dbName: string = 'sequence-indexeddb',
|
|
22
|
+
private readonly env?: CoreEnv,
|
|
23
|
+
) {
|
|
20
24
|
this.dbName = dbName
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
private getIndexedDB(): IDBFactory {
|
|
28
|
+
const globalObj = globalThis as any
|
|
29
|
+
const indexedDb = this.env?.indexedDB ?? globalObj.indexedDB ?? globalObj.window?.indexedDB
|
|
30
|
+
if (!indexedDb) {
|
|
31
|
+
throw new Error('indexedDB is not available')
|
|
32
|
+
}
|
|
33
|
+
return indexedDb
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
private async openDB(): Promise<IDBDatabase> {
|
|
24
37
|
if (this._db) return this._db
|
|
25
38
|
|
|
26
39
|
return new Promise((resolve, reject) => {
|
|
27
|
-
const request =
|
|
40
|
+
const request = this.getIndexedDB().open(this.dbName, DB_VERSION)
|
|
28
41
|
|
|
29
42
|
request.onupgradeneeded = () => {
|
|
30
43
|
const db = request.result
|
|
@@ -4,10 +4,16 @@ import { Provider } from '../index.js'
|
|
|
4
4
|
|
|
5
5
|
export class DevHttpProvider implements Provider {
|
|
6
6
|
private readonly baseUrl: string
|
|
7
|
+
private readonly fetcher: typeof fetch
|
|
7
8
|
|
|
8
|
-
constructor(baseUrl: string) {
|
|
9
|
+
constructor(baseUrl: string, fetcher?: typeof fetch) {
|
|
9
10
|
// Remove trailing slash if present
|
|
10
11
|
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
|
|
12
|
+
const resolvedFetch = fetcher ?? (globalThis as any).fetch
|
|
13
|
+
if (!resolvedFetch) {
|
|
14
|
+
throw new Error('fetch is not available')
|
|
15
|
+
}
|
|
16
|
+
this.fetcher = resolvedFetch
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
private async request<T>(method: 'GET' | 'POST', path: string, body?: any): Promise<T> {
|
|
@@ -24,7 +30,7 @@ export class DevHttpProvider implements Provider {
|
|
|
24
30
|
|
|
25
31
|
let response: Response
|
|
26
32
|
try {
|
|
27
|
-
response = await
|
|
33
|
+
response = await this.fetcher(url, options)
|
|
28
34
|
} catch (networkError) {
|
|
29
35
|
// Handle immediate network errors (e.g., DNS resolution failure, refused connection)
|
|
30
36
|
console.error(`Network error during ${method} request to ${url}:`, networkError)
|
|
@@ -38,12 +44,12 @@ export class DevHttpProvider implements Provider {
|
|
|
38
44
|
const errorText = await response.text()
|
|
39
45
|
const errorJson = await Utils.fromJSON(errorText)
|
|
40
46
|
errorPayload = { ...errorPayload, ...errorJson }
|
|
41
|
-
} catch
|
|
47
|
+
} catch {
|
|
42
48
|
try {
|
|
43
49
|
// If JSON parsing fails, try getting text for better error message
|
|
44
50
|
const errorText = await response.text()
|
|
45
51
|
errorPayload.body = errorText
|
|
46
|
-
} catch
|
|
52
|
+
} catch {
|
|
47
53
|
// Ignore if reading text also fails
|
|
48
54
|
}
|
|
49
55
|
}
|
|
@@ -99,7 +105,7 @@ export class DevHttpProvider implements Provider {
|
|
|
99
105
|
throw new Error(
|
|
100
106
|
`Failed to parse JSON response from server. Status: ${response.status}. Body: "${text}". Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
101
107
|
)
|
|
102
|
-
} catch
|
|
108
|
+
} catch {
|
|
103
109
|
throw new Error(
|
|
104
110
|
`Failed to parse JSON response from server and could not read response body as text. Status: ${response.status}. Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
105
111
|
)
|
|
@@ -9,13 +9,17 @@ import {
|
|
|
9
9
|
TransactionRequest,
|
|
10
10
|
} from 'ox'
|
|
11
11
|
import { normalizeAddressKeys, Provider as ProviderInterface } from '../index.js'
|
|
12
|
-
import { Sessions, SignatureType } from './sessions.gen.js'
|
|
12
|
+
import { Sessions, SignatureType, type Fetch } from './sessions.gen.js'
|
|
13
13
|
|
|
14
14
|
export class Provider implements ProviderInterface {
|
|
15
15
|
private readonly service: Sessions
|
|
16
16
|
|
|
17
|
-
constructor(host = 'https://keymachine.sequence.app') {
|
|
18
|
-
|
|
17
|
+
constructor(host = 'https://keymachine.sequence.app', fetcher?: Fetch) {
|
|
18
|
+
const resolvedFetch = fetcher ?? (globalThis as any).fetch
|
|
19
|
+
if (!resolvedFetch) {
|
|
20
|
+
throw new Error('fetch is not available')
|
|
21
|
+
}
|
|
22
|
+
this.service = new Sessions(host, resolvedFetch)
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
async getConfiguration(imageHash: Hex.Hex): Promise<Config.Config | undefined> {
|
|
@@ -186,7 +190,9 @@ export class Provider implements ProviderInterface {
|
|
|
186
190
|
case SignatureType.SapientCompact:
|
|
187
191
|
throw new Error(`unexpected compact sapient signature by ${signer}`)
|
|
188
192
|
}
|
|
189
|
-
} catch {
|
|
193
|
+
} catch {
|
|
194
|
+
// ignore
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
async getWitnessForSapient(
|
|
@@ -221,7 +227,9 @@ export class Provider implements ProviderInterface {
|
|
|
221
227
|
signature: { type: 'sapient_compact', address: signer, data: witness.signature },
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
|
-
} catch {
|
|
230
|
+
} catch {
|
|
231
|
+
// ignore
|
|
232
|
+
}
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
async getConfigurationUpdates(
|
|
@@ -380,7 +388,7 @@ const recoverSapientSignatureCompactFunction = AbiFunction.from(recoverSapientSi
|
|
|
380
388
|
class PasskeySignatureValidator implements oxProvider.Provider {
|
|
381
389
|
request: oxProvider.Provider['request'] = (async (request) => {
|
|
382
390
|
switch (request.method) {
|
|
383
|
-
case 'eth_call':
|
|
391
|
+
case 'eth_call': {
|
|
384
392
|
if (!request.params || !Array.isArray(request.params) || request.params.length === 0) {
|
|
385
393
|
throw new Error('eth_call requires transaction parameters')
|
|
386
394
|
}
|
|
@@ -406,6 +414,7 @@ class PasskeySignatureValidator implements oxProvider.Provider {
|
|
|
406
414
|
} else {
|
|
407
415
|
throw new Error(`invalid passkey signature ${signature} for digest ${digest}`)
|
|
408
416
|
}
|
|
417
|
+
}
|
|
409
418
|
|
|
410
419
|
default:
|
|
411
420
|
throw new Error(`method ${request.method} not implemented`)
|
package/src/wallet.ts
CHANGED
|
@@ -355,7 +355,7 @@ export class Wallet {
|
|
|
355
355
|
throw new Error('4337 is not enabled in this wallet')
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
const noncePromise = this.get4337Nonce(provider, status.context.capabilities
|
|
358
|
+
const noncePromise = this.get4337Nonce(provider, status.context.capabilities.erc4337.entrypoint, space)
|
|
359
359
|
|
|
360
360
|
// If the wallet is not deployed, then we need to include the initCode on
|
|
361
361
|
// the 4337 transaction
|
|
@@ -570,7 +570,7 @@ export class Wallet {
|
|
|
570
570
|
if (typeof message !== 'string') {
|
|
571
571
|
encodedMessage = TypedData.encode(message)
|
|
572
572
|
} else {
|
|
573
|
-
|
|
573
|
+
const hexMessage = Hex.validate(message) ? message : Hex.fromString(message)
|
|
574
574
|
const messageSize = Hex.size(hexMessage)
|
|
575
575
|
encodedMessage = Hex.concat(Hex.fromString(`${`\x19Ethereum Signed Message:\n${messageSize}`}`), hexMessage)
|
|
576
576
|
}
|
package/test/constants.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { config as dotenvConfig } from 'dotenv'
|
|
2
2
|
import { Abi, AbiEvent, Address } from 'ox'
|
|
3
3
|
|
|
4
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
4
5
|
const envFile = process.env.CI ? '.env.test' : '.env.test.local'
|
|
5
6
|
dotenvConfig({ path: envFile })
|
|
6
7
|
|
|
8
|
+
// Contracts are deployed on Arbitrum
|
|
9
|
+
|
|
7
10
|
// Requires https://example.com redirectUrl
|
|
8
11
|
export const EMITTER_ADDRESS1: Address.Address = '0xad90eB52BC180Bd9f66f50981E196f3E996278D3'
|
|
9
12
|
// Requires https://another-example.com redirectUrl
|
|
@@ -16,4 +19,5 @@ export const EMITTER_EVENT_TOPICS = [
|
|
|
16
19
|
export const USDC_ADDRESS: Address.Address = '0xaf88d065e77c8cc2239327c5edb3a432268e5831'
|
|
17
20
|
|
|
18
21
|
// Environment variables
|
|
22
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
19
23
|
export const LOCAL_RPC_URL = process.env.LOCAL_RPC_URL || 'http://localhost:8545'
|
package/test/envelope.test.ts
CHANGED
|
@@ -6,7 +6,6 @@ import * as Envelope from '../src/envelope.js'
|
|
|
6
6
|
|
|
7
7
|
// Test addresses and data
|
|
8
8
|
const TEST_ADDRESS_1 = Address.from('0x1234567890123456789012345678901234567890')
|
|
9
|
-
const TEST_ADDRESS_2 = Address.from('0xabcdefabcdefabcdefabcdefabcdefabcdefabcd')
|
|
10
9
|
const TEST_ADDRESS_3 = Address.from('0x9876543210987654321098765432109876543210')
|
|
11
10
|
const TEST_WALLET = Address.from('0xfedcbafedcbafedcbafedcbafedcbafedcbafe00')
|
|
12
11
|
const TEST_IMAGE_HASH = Hex.from('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef')
|