Skip to content

Commit 939e50a

Browse files
Add env injection for core and wdk
1 parent 3411232 commit 939e50a

File tree

22 files changed

+694
-124
lines changed

22 files changed

+694
-124
lines changed

packages/wallet/core/src/bundler/bundlers/pimlico.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ export class PimlicoBundler implements Bundler {
2121

2222
public readonly provider: Provider.Provider
2323
public readonly bundlerRpcUrl: string
24+
private readonly fetcher: typeof fetch
2425

25-
constructor(bundlerRpcUrl: string, provider: Provider.Provider | string) {
26+
constructor(bundlerRpcUrl: string, provider: Provider.Provider | string, fetcher?: typeof fetch) {
2627
this.id = `pimlico-erc4337-${bundlerRpcUrl}`
2728
this.provider = typeof provider === 'string' ? Provider.from(RpcTransport.fromHttp(provider)) : provider
2829
this.bundlerRpcUrl = bundlerRpcUrl
30+
const resolvedFetch = fetcher ?? (globalThis as any).fetch
31+
if (!resolvedFetch) {
32+
throw new Error('fetch is not available')
33+
}
34+
this.fetcher = resolvedFetch
2935
}
3036

3137
async isAvailable(entrypoint: Address.Address, chainId: number): Promise<boolean> {
@@ -165,7 +171,7 @@ export class PimlicoBundler implements Bundler {
165171

166172
private async bundlerRpc<T>(method: string, params: any[]): Promise<T> {
167173
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
168-
const res = await fetch(this.bundlerRpcUrl, {
174+
const res = await this.fetcher(this.bundlerRpcUrl, {
169175
method: 'POST',
170176
headers: { 'content-type': 'application/json' },
171177
body,

packages/wallet/core/src/env.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export type StorageLike = {
2+
getItem: (key: string) => string | null
3+
setItem: (key: string, value: string) => void
4+
removeItem: (key: string) => void
5+
}
6+
7+
export type CryptoLike = {
8+
subtle: SubtleCrypto
9+
getRandomValues: <T extends ArrayBufferView>(array: T) => T
10+
}
11+
12+
export type TextEncodingLike = {
13+
TextEncoder: typeof TextEncoder
14+
TextDecoder: typeof TextDecoder
15+
}
16+
17+
export type CoreEnv = {
18+
fetch?: typeof fetch
19+
crypto?: CryptoLike
20+
storage?: StorageLike
21+
indexedDB?: IDBFactory
22+
text?: Partial<TextEncodingLike>
23+
}
24+
25+
function isStorageLike(value: unknown): value is StorageLike {
26+
if (!value || typeof value !== 'object') return false
27+
const candidate = value as StorageLike
28+
return (
29+
typeof candidate.getItem === 'function' &&
30+
typeof candidate.setItem === 'function' &&
31+
typeof candidate.removeItem === 'function'
32+
)
33+
}
34+
35+
export function resolveCoreEnv(env?: CoreEnv): CoreEnv {
36+
const globalObj = globalThis as any
37+
const windowObj = typeof window !== 'undefined' ? window : (globalObj.window ?? {})
38+
let storage: StorageLike | undefined
39+
let text: Partial<TextEncodingLike> | undefined
40+
41+
if (isStorageLike(env?.storage)) {
42+
storage = env.storage
43+
} else if (isStorageLike(windowObj.localStorage)) {
44+
storage = windowObj.localStorage
45+
} else if (isStorageLike(globalObj.localStorage)) {
46+
storage = globalObj.localStorage
47+
}
48+
49+
if (env?.text) {
50+
if (!env.text.TextEncoder || !env.text.TextDecoder) {
51+
throw new Error('env.text must provide both TextEncoder and TextDecoder')
52+
}
53+
text = env.text
54+
} else {
55+
text = {
56+
TextEncoder: windowObj.TextEncoder ?? globalObj.TextEncoder,
57+
TextDecoder: windowObj.TextDecoder ?? globalObj.TextDecoder,
58+
}
59+
}
60+
61+
return {
62+
fetch: env?.fetch ?? windowObj.fetch ?? globalObj.fetch,
63+
crypto: env?.crypto ?? windowObj.crypto ?? globalObj.crypto,
64+
storage,
65+
indexedDB: env?.indexedDB ?? windowObj.indexedDB ?? globalObj.indexedDB,
66+
text,
67+
}
68+
}

packages/wallet/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * as State from './state/index.js'
55
export * as Bundler from './bundler/index.js'
66
export * as Envelope from './envelope.js'
77
export * as Utils from './utils/index.js'
8+
export * from './env.js'
89
export {
910
type ExplicitSessionConfig,
1011
type ExplicitSession,

packages/wallet/core/src/signers/passkey.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@ import { WebAuthnP256 } from 'ox'
55
import { State } from '../index.js'
66
import { SapientSigner, Witnessable } from './index.js'
77

8+
export type WebAuthnLike = Pick<typeof WebAuthnP256, 'createCredential' | 'sign'>
9+
810
export type PasskeyOptions = {
911
extensions: Pick<Extensions.Extensions, 'passkeys'>
1012
publicKey: Extensions.Passkeys.PublicKey
1113
credentialId: string
1214
embedMetadata?: boolean
1315
metadata?: Extensions.Passkeys.PasskeyMetadata
16+
webauthn?: WebAuthnLike
1417
}
1518

1619
export type CreatePasskeyOptions = {
1720
stateProvider?: State.Provider
1821
requireUserVerification?: boolean
1922
credentialName?: string
2023
embedMetadata?: boolean
24+
webauthn?: WebAuthnLike
25+
}
26+
27+
export type FindPasskeyOptions = {
28+
webauthn?: WebAuthnLike
2129
}
2230

2331
export type WitnessMessage = {
@@ -45,6 +53,7 @@ export class Passkey implements SapientSigner, Witnessable {
4553
public readonly imageHash: Hex.Hex
4654
public readonly embedMetadata: boolean
4755
public readonly metadata?: Extensions.Passkeys.PasskeyMetadata
56+
private readonly webauthn: WebAuthnLike
4857

4958
constructor(options: PasskeyOptions) {
5059
this.address = options.extensions.passkeys
@@ -53,13 +62,15 @@ export class Passkey implements SapientSigner, Witnessable {
5362
this.embedMetadata = options.embedMetadata ?? false
5463
this.imageHash = Extensions.Passkeys.rootFor(options.publicKey)
5564
this.metadata = options.metadata
65+
this.webauthn = options.webauthn ?? WebAuthnP256
5666
}
5767

5868
static async loadFromWitness(
5969
stateReader: State.Reader,
6070
extensions: Pick<Extensions.Extensions, 'passkeys'>,
6171
wallet: Address.Address,
6272
imageHash: Hex.Hex,
73+
options?: FindPasskeyOptions,
6374
) {
6475
// In the witness we will find the public key, and may find the credential id
6576
const witness = await stateReader.getWitnessForSapient(wallet, extensions.passkeys, imageHash)
@@ -90,13 +101,15 @@ export class Passkey implements SapientSigner, Witnessable {
90101
publicKey: message.publicKey,
91102
embedMetadata: decodedSignature.embedMetadata,
92103
metadata,
104+
webauthn: options?.webauthn,
93105
})
94106
}
95107

96108
static async create(extensions: Pick<Extensions.Extensions, 'passkeys'>, options?: CreatePasskeyOptions) {
109+
const webauthn = options?.webauthn ?? WebAuthnP256
97110
const name = options?.credentialName ?? `Sequence (${Date.now()})`
98111

99-
const credential = await WebAuthnP256.createCredential({
112+
const credential = await webauthn.createCredential({
100113
user: {
101114
name,
102115
},
@@ -120,6 +133,7 @@ export class Passkey implements SapientSigner, Witnessable {
120133
},
121134
embedMetadata: options?.embedMetadata,
122135
metadata,
136+
webauthn,
123137
})
124138

125139
if (options?.stateProvider) {
@@ -132,8 +146,10 @@ export class Passkey implements SapientSigner, Witnessable {
132146
static async find(
133147
stateReader: State.Reader,
134148
extensions: Pick<Extensions.Extensions, 'passkeys'>,
149+
options?: FindPasskeyOptions,
135150
): Promise<Passkey | undefined> {
136-
const response = await WebAuthnP256.sign({ challenge: Hex.random(32) })
151+
const webauthn = options?.webauthn ?? WebAuthnP256
152+
const response = await webauthn.sign({ challenge: Hex.random(32) })
137153
if (!response.raw) throw new Error('No credential returned')
138154

139155
const authenticatorDataBytes = Bytes.fromHex(response.metadata.authenticatorData)
@@ -218,7 +234,7 @@ export class Passkey implements SapientSigner, Witnessable {
218234
console.warn('Multiple signers found for passkey', flattened)
219235
}
220236

221-
return Passkey.loadFromWitness(stateReader, extensions, flattened[0]!.wallet, flattened[0]!.imageHash)
237+
return Passkey.loadFromWitness(stateReader, extensions, flattened[0]!.wallet, flattened[0]!.imageHash, options)
222238
}
223239

224240
async signSapient(
@@ -234,7 +250,7 @@ export class Passkey implements SapientSigner, Witnessable {
234250

235251
const challenge = Hex.fromBytes(Payload.hash(wallet, chainId, payload))
236252

237-
const response = await WebAuthnP256.sign({
253+
const response = await this.webauthn.sign({
238254
challenge,
239255
credentialId: this.credentialId,
240256
userVerification: this.publicKey.requireUserVerification ? 'required' : 'discouraged',

packages/wallet/core/src/signers/pk/encrypted.ts

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Hex, Address, PublicKey, Secp256k1, Bytes } from 'ox'
2+
import { resolveCoreEnv, type CoreEnv, type CryptoLike, type StorageLike, type TextEncodingLike } from '../../env.js'
23
import { PkStore } from './index.js'
34

45
export interface EncryptedData {
@@ -17,6 +18,7 @@ export class EncryptedPksDb {
1718
constructor(
1819
private readonly localStorageKeyPrefix: string = 'e_pk_key_',
1920
tableName: string = 'e_pk',
21+
private readonly env?: CoreEnv,
2022
) {
2123
this.tableName = tableName
2224
}
@@ -25,9 +27,59 @@ export class EncryptedPksDb {
2527
return `pk_${address.toLowerCase()}`
2628
}
2729

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+
2880
private openDB(): Promise<IDBDatabase> {
2981
return new Promise((resolve, reject) => {
30-
const request = indexedDB.open(this.dbName, this.dbVersion)
82+
const request = this.getIndexedDB().open(this.dbName, this.dbVersion)
3183
request.onupgradeneeded = () => {
3284
const db = request.result
3385
if (!db.objectStoreNames.contains(this.tableName)) {
@@ -73,7 +125,11 @@ export class EncryptedPksDb {
73125
}
74126

75127
async generateAndStore(): Promise<EncryptedData> {
76-
const encryptionKey = await window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
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, [
77133
'encrypt',
78134
'decrypt',
79135
])
@@ -84,13 +140,13 @@ export class EncryptedPksDb {
84140
const address = Address.fromPublicKey(publicKey)
85141
const keyPointer = this.localStorageKeyPrefix + address
86142

87-
const exportedKey = await window.crypto.subtle.exportKey('jwk', encryptionKey)
88-
window.localStorage.setItem(keyPointer, JSON.stringify(exportedKey))
143+
const exportedKey = await crypto.subtle.exportKey('jwk', encryptionKey)
144+
storage.setItem(keyPointer, JSON.stringify(exportedKey))
89145

90-
const encoder = new TextEncoder()
146+
const encoder = new TextEncoderCtor()
91147
const encodedPk = encoder.encode(privateKey)
92-
const iv = window.crypto.getRandomValues(new Uint8Array(12))
93-
const encryptedBuffer = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv }, encryptionKey, encodedPk)
148+
const iv = crypto.getRandomValues(new Uint8Array(12))
149+
const encryptedBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, encryptionKey, encodedPk)
94150

95151
const encrypted: EncryptedData = {
96152
iv,
@@ -113,7 +169,7 @@ export class EncryptedPksDb {
113169
async getEncryptedPkStore(address: Address.Address): Promise<EncryptedPkStore | undefined> {
114170
const entry = await this.getEncryptedEntry(address)
115171
if (!entry) return
116-
return new EncryptedPkStore(entry)
172+
return new EncryptedPkStore(entry, this.env)
117173
}
118174

119175
async listAddresses(): Promise<Address.Address[]> {
@@ -125,12 +181,41 @@ export class EncryptedPksDb {
125181
const dbKey = this.computeDbKey(address)
126182
await this.putData(dbKey, undefined)
127183
const keyPointer = this.localStorageKeyPrefix + address
128-
window.localStorage.removeItem(keyPointer)
184+
this.getStorage().removeItem(keyPointer)
129185
}
130186
}
131187

132188
export class EncryptedPkStore implements PkStore {
133-
constructor(private readonly encrypted: EncryptedData) {}
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+
}
134219

135220
address(): Address.Address {
136221
return this.encrypted.address
@@ -141,16 +226,20 @@ export class EncryptedPkStore implements PkStore {
141226
}
142227

143228
async signDigest(digest: Bytes.Bytes): Promise<{ r: bigint; s: bigint; yParity: number }> {
144-
const keyJson = window.localStorage.getItem(this.encrypted.keyPointer)
229+
const storage = this.getStorage()
230+
const crypto = this.getCrypto()
231+
const TextDecoderCtor = this.getTextDecoderCtor()
232+
233+
const keyJson = storage.getItem(this.encrypted.keyPointer)
145234
if (!keyJson) throw new Error('Encryption key not found in localStorage')
146235
const jwk = JSON.parse(keyJson)
147-
const encryptionKey = await window.crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, false, ['decrypt'])
148-
const decryptedBuffer = await window.crypto.subtle.decrypt(
236+
const encryptionKey = await crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, false, ['decrypt'])
237+
const decryptedBuffer = await crypto.subtle.decrypt(
149238
{ name: 'AES-GCM', iv: this.encrypted.iv },
150239
encryptionKey,
151240
this.encrypted.data,
152241
)
153-
const decoder = new TextDecoder()
242+
const decoder = new TextDecoderCtor()
154243
const privateKey = decoder.decode(decryptedBuffer) as Hex.Hex
155244
return Secp256k1.sign({ payload: digest, privateKey })
156245
}

0 commit comments

Comments
 (0)