@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
@@ -0,0 +1,110 @@
1
+ import { Payload } from '@0xsequence/wallet-primitives'
2
+ import { Address, Hex, Provider, Secp256k1, TransactionEnvelopeEip1559, TransactionReceipt } from 'ox'
3
+ import { LocalRelayer } from './local.js'
4
+ import { FeeOption, FeeQuote, OperationStatus, Relayer } from './relayer.js'
5
+
6
+ export class PkRelayer implements Relayer {
7
+ public readonly id = 'pk'
8
+ private readonly relayer: LocalRelayer
9
+
10
+ constructor(
11
+ privateKey: Hex.Hex,
12
+ private readonly provider: Provider.Provider,
13
+ ) {
14
+ const relayerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey }))
15
+ this.relayer = new LocalRelayer({
16
+ sendTransaction: async (args, chainId) => {
17
+ const providerChainId = BigInt(await this.provider.request({ method: 'eth_chainId' }))
18
+ if (providerChainId !== chainId) {
19
+ throw new Error('Provider chain id does not match relayer chain id')
20
+ }
21
+
22
+ const oxArgs = { ...args, to: args.to as `0x${string}`, data: args.data as `0x${string}` }
23
+ // Estimate gas with a safety buffer
24
+ const estimatedGas = BigInt(await this.provider.request({ method: 'eth_estimateGas', params: [oxArgs] }))
25
+ const safeGasLimit = estimatedGas > 21000n ? (estimatedGas * 12n) / 10n : 50000n
26
+
27
+ // Get base fee and priority fee
28
+ const baseFee = BigInt(await this.provider.request({ method: 'eth_gasPrice' }))
29
+ const priorityFee = 100000000n // 0.1 gwei priority fee
30
+ const maxFeePerGas = baseFee + priorityFee
31
+
32
+ // Check sender have enough balance
33
+ const senderBalance = BigInt(
34
+ await this.provider.request({ method: 'eth_getBalance', params: [relayerAddress, 'latest'] }),
35
+ )
36
+ if (senderBalance < maxFeePerGas * safeGasLimit) {
37
+ console.log('Sender balance:', senderBalance.toString(), 'wei')
38
+ throw new Error('Sender has insufficient balance to pay for gas')
39
+ }
40
+ const nonce = BigInt(
41
+ await this.provider.request({
42
+ method: 'eth_getTransactionCount',
43
+ params: [relayerAddress, 'latest'],
44
+ }),
45
+ )
46
+
47
+ // Build the relay envelope
48
+ const relayEnvelope = TransactionEnvelopeEip1559.from({
49
+ chainId: Number(chainId),
50
+ type: 'eip1559',
51
+ from: relayerAddress,
52
+ to: oxArgs.to,
53
+ data: oxArgs.data,
54
+ gas: safeGasLimit,
55
+ maxFeePerGas: maxFeePerGas,
56
+ maxPriorityFeePerGas: priorityFee,
57
+ nonce: nonce,
58
+ value: 0n,
59
+ })
60
+ const relayerSignature = Secp256k1.sign({
61
+ payload: TransactionEnvelopeEip1559.getSignPayload(relayEnvelope),
62
+ privateKey: privateKey,
63
+ })
64
+ const signedRelayEnvelope = TransactionEnvelopeEip1559.from(relayEnvelope, {
65
+ signature: relayerSignature,
66
+ })
67
+ const tx = await this.provider.request({
68
+ method: 'eth_sendRawTransaction',
69
+ params: [TransactionEnvelopeEip1559.serialize(signedRelayEnvelope)],
70
+ })
71
+ return tx
72
+ },
73
+ getTransactionReceipt: async (txHash: string, chainId: bigint) => {
74
+ Hex.assert(txHash)
75
+
76
+ const providerChainId = BigInt(await this.provider.request({ method: 'eth_chainId' }))
77
+ if (providerChainId !== chainId) {
78
+ throw new Error('Provider chain id does not match relayer chain id')
79
+ }
80
+
81
+ const rpcReceipt = await this.provider.request({ method: 'eth_getTransactionReceipt', params: [txHash] })
82
+ if (!rpcReceipt) {
83
+ return 'unknown'
84
+ }
85
+ const receipt = TransactionReceipt.fromRpc(rpcReceipt)
86
+ return receipt.status === 'success' ? 'success' : 'failed'
87
+ },
88
+ })
89
+ }
90
+
91
+ feeOptions(
92
+ wallet: Address.Address,
93
+ chainId: bigint,
94
+ calls: Payload.Call[],
95
+ ): Promise<{ options: FeeOption[]; quote?: FeeQuote }> {
96
+ return this.relayer.feeOptions(wallet, chainId, calls)
97
+ }
98
+
99
+ async relay(to: Address.Address, data: Hex.Hex, chainId: bigint, _?: FeeQuote): Promise<{ opHash: Hex.Hex }> {
100
+ const providerChainId = BigInt(await this.provider.request({ method: 'eth_chainId' }))
101
+ if (providerChainId !== chainId) {
102
+ throw new Error('Provider chain id does not match relayer chain id')
103
+ }
104
+ return this.relayer.relay(to, data, chainId)
105
+ }
106
+
107
+ status(opHash: Hex.Hex, chainId: bigint): Promise<OperationStatus> {
108
+ return this.relayer.status(opHash, chainId)
109
+ }
110
+ }
@@ -0,0 +1,52 @@
1
+ import { Payload } from '@0xsequence/wallet-primitives'
2
+ import { Address, Hex } from 'ox'
3
+
4
+ export interface FeeOption {
5
+ token: Address.Address
6
+ to: string
7
+ value: string
8
+ gasLimit: number
9
+ }
10
+
11
+ export interface FeeQuote {
12
+ _tag: 'FeeQuote'
13
+ _quote: unknown
14
+ }
15
+
16
+ export type OperationUknownStatus = {
17
+ status: 'unknown'
18
+ }
19
+
20
+ export type OperationPendingStatus = {
21
+ status: 'pending'
22
+ }
23
+
24
+ export type OperationConfirmedStatus = {
25
+ status: 'confirmed'
26
+ transactionHash: Hex.Hex
27
+ }
28
+
29
+ export type OperationFailedStatus = {
30
+ status: 'failed'
31
+ reason: string
32
+ }
33
+
34
+ export type OperationStatus =
35
+ | OperationUknownStatus
36
+ | OperationPendingStatus
37
+ | OperationConfirmedStatus
38
+ | OperationFailedStatus
39
+
40
+ export interface Relayer {
41
+ id: string
42
+
43
+ feeOptions(
44
+ wallet: Address.Address,
45
+ chainId: bigint,
46
+ calls: Payload.Call[],
47
+ ): Promise<{ options: FeeOption[]; quote?: FeeQuote }>
48
+
49
+ relay(to: Address.Address, data: Hex.Hex, chainId: bigint, quote?: FeeQuote): Promise<{ opHash: Hex.Hex }>
50
+
51
+ status(opHash: Hex.Hex, chainId: bigint): Promise<OperationStatus>
52
+ }
@@ -0,0 +1,44 @@
1
+ import { Config, Payload, Signature } from '@0xsequence/wallet-primitives'
2
+ import { Address, Hex } from 'ox'
3
+ import * as State from '../state/index.js'
4
+
5
+ export * as Pk from './pk/index.js'
6
+ export * as Passkey from './passkey.js'
7
+ export * as Session from './session/index.js'
8
+ export * from './session-manager.js'
9
+
10
+ export interface Signer {
11
+ readonly address: MaybePromise<Address.Address>
12
+
13
+ sign: (
14
+ wallet: Address.Address,
15
+ chainId: bigint,
16
+ payload: Payload.Parented,
17
+ ) => Config.SignerSignature<Signature.SignatureOfSignerLeaf>
18
+ }
19
+
20
+ export interface SapientSigner {
21
+ readonly address: MaybePromise<Address.Address>
22
+ readonly imageHash: MaybePromise<Hex.Hex | undefined>
23
+
24
+ signSapient: (
25
+ wallet: Address.Address,
26
+ chainId: bigint,
27
+ payload: Payload.Parented,
28
+ imageHash: Hex.Hex,
29
+ ) => Config.SignerSignature<Signature.SignatureOfSapientSignerLeaf>
30
+ }
31
+
32
+ export interface Witnessable {
33
+ witness: (stateWriter: State.Writer, wallet: Address.Address, extra?: Object) => Promise<void>
34
+ }
35
+
36
+ type MaybePromise<T> = T | Promise<T>
37
+
38
+ export function isSapientSigner(signer: Signer | SapientSigner): signer is SapientSigner {
39
+ return 'signSapient' in signer
40
+ }
41
+
42
+ export function isSigner(signer: Signer | SapientSigner): signer is Signer {
43
+ return 'sign' in signer
44
+ }
@@ -0,0 +1,284 @@
1
+ import { Hex, Bytes, Address, P256, Hash } from 'ox'
2
+ import { Payload, Extensions } from '@0xsequence/wallet-primitives'
3
+ import type { Signature as SignatureTypes } from '@0xsequence/wallet-primitives'
4
+ import { WebAuthnP256 } from 'ox'
5
+ import { State } from '../index.js'
6
+ import { SapientSigner, Witnessable } from './index.js'
7
+
8
+ export type PasskeyOptions = {
9
+ extensions: Pick<Extensions.Extensions, 'passkeys'>
10
+ publicKey: Extensions.Passkeys.PublicKey
11
+ credentialId: string
12
+ embedMetadata?: boolean
13
+ metadata?: Extensions.Passkeys.PasskeyMetadata
14
+ }
15
+
16
+ export type CreaetePasskeyOptions = {
17
+ stateProvider?: State.Provider
18
+ requireUserVerification?: boolean
19
+ credentialName?: string
20
+ embedMetadata?: boolean
21
+ }
22
+
23
+ export type WitnessMessage = {
24
+ action: 'consent-to-be-part-of-wallet'
25
+ wallet: Address.Address
26
+ publicKey: Extensions.Passkeys.PublicKey
27
+ timestamp: number
28
+ metadata?: Extensions.Passkeys.PasskeyMetadata
29
+ }
30
+
31
+ export function isWitnessMessage(message: unknown): message is WitnessMessage {
32
+ return (
33
+ typeof message === 'object' &&
34
+ message !== null &&
35
+ 'action' in message &&
36
+ message.action === 'consent-to-be-part-of-wallet'
37
+ )
38
+ }
39
+
40
+ export class Passkey implements SapientSigner, Witnessable {
41
+ public readonly credentialId: string
42
+
43
+ public readonly publicKey: Extensions.Passkeys.PublicKey
44
+ public readonly address: Address.Address
45
+ public readonly imageHash: Hex.Hex
46
+ public readonly embedMetadata: boolean
47
+ public readonly metadata?: Extensions.Passkeys.PasskeyMetadata
48
+
49
+ constructor(options: PasskeyOptions) {
50
+ this.address = options.extensions.passkeys
51
+ this.publicKey = options.publicKey
52
+ this.credentialId = options.credentialId
53
+ this.embedMetadata = options.embedMetadata ?? false
54
+ this.imageHash = Extensions.Passkeys.rootFor(options.publicKey)
55
+ this.metadata = options.metadata
56
+ }
57
+
58
+ static async loadFromWitness(
59
+ stateReader: State.Reader,
60
+ extensions: Pick<Extensions.Extensions, 'passkeys'>,
61
+ wallet: Address.Address,
62
+ imageHash: Hex.Hex,
63
+ ) {
64
+ // In the witness we will find the public key, and may find the credential id
65
+ const witness = await stateReader.getWitnessForSapient(wallet, extensions.passkeys, imageHash)
66
+ if (!witness) {
67
+ throw new Error('Witness for wallet not found')
68
+ }
69
+
70
+ const payload = witness.payload
71
+ if (!Payload.isMessage(payload)) {
72
+ throw new Error('Witness payload is not a message')
73
+ }
74
+
75
+ const message = JSON.parse(Hex.toString(payload.message))
76
+ if (!isWitnessMessage(message)) {
77
+ throw new Error('Witness payload is not a witness message')
78
+ }
79
+
80
+ const metadata = message.publicKey.metadata || message.metadata
81
+ if (typeof metadata === 'string' || !metadata) {
82
+ throw new Error('Metadata does not contain credential id')
83
+ }
84
+
85
+ const decodedSignature = Extensions.Passkeys.decode(Bytes.fromHex(witness.signature.data))
86
+
87
+ return new Passkey({
88
+ credentialId: metadata.credentialId,
89
+ extensions,
90
+ publicKey: message.publicKey,
91
+ embedMetadata: decodedSignature.embedMetadata,
92
+ metadata,
93
+ })
94
+ }
95
+
96
+ static async create(extensions: Pick<Extensions.Extensions, 'passkeys'>, options?: CreaetePasskeyOptions) {
97
+ const name = options?.credentialName ?? `Sequence (${Date.now()})`
98
+
99
+ const credential = await WebAuthnP256.createCredential({
100
+ user: {
101
+ name,
102
+ },
103
+ })
104
+
105
+ const x = Hex.fromNumber(credential.publicKey.x)
106
+ const y = Hex.fromNumber(credential.publicKey.y)
107
+
108
+ const metadata = {
109
+ credentialId: credential.id,
110
+ }
111
+
112
+ const passkey = new Passkey({
113
+ credentialId: credential.id,
114
+ extensions,
115
+ publicKey: {
116
+ requireUserVerification: options?.requireUserVerification ?? true,
117
+ x,
118
+ y,
119
+ metadata: options?.embedMetadata ? metadata : undefined,
120
+ },
121
+ embedMetadata: options?.embedMetadata,
122
+ metadata,
123
+ })
124
+
125
+ if (options?.stateProvider) {
126
+ await options.stateProvider.saveTree(Extensions.Passkeys.toTree(passkey.publicKey))
127
+ }
128
+
129
+ return passkey
130
+ }
131
+
132
+ static async find(
133
+ stateReader: State.Reader,
134
+ extensions: Pick<Extensions.Extensions, 'passkeys'>,
135
+ ): Promise<Passkey | undefined> {
136
+ const response = await WebAuthnP256.sign({ challenge: Hex.random(32) })
137
+ if (!response.raw) throw new Error('No credential returned')
138
+
139
+ const authenticatorDataBytes = Bytes.fromHex(response.metadata.authenticatorData)
140
+ const clientDataHash = Hash.sha256(Bytes.fromString(response.metadata.clientDataJSON), { as: 'Bytes' })
141
+ const messageSignedByAuthenticator = Bytes.concat(authenticatorDataBytes, clientDataHash)
142
+
143
+ const messageHash = Hash.sha256(messageSignedByAuthenticator, { as: 'Bytes' }) // Use Bytes output
144
+
145
+ const publicKey1 = P256.recoverPublicKey({
146
+ payload: messageHash,
147
+ signature: {
148
+ r: BigInt(response.signature.r),
149
+ s: BigInt(response.signature.s),
150
+ yParity: 0,
151
+ },
152
+ })
153
+
154
+ const publicKey2 = P256.recoverPublicKey({
155
+ payload: messageHash,
156
+ signature: {
157
+ r: BigInt(response.signature.r),
158
+ s: BigInt(response.signature.s),
159
+ yParity: 1,
160
+ },
161
+ })
162
+
163
+ // Compute the imageHash for all public key combinations
164
+ // - requireUserVerification: true / false
165
+ // - embedMetadata: true / false
166
+
167
+ const base1 = {
168
+ x: Hex.fromNumber(publicKey1.x),
169
+ y: Hex.fromNumber(publicKey1.y),
170
+ }
171
+
172
+ const base2 = {
173
+ x: Hex.fromNumber(publicKey2.x),
174
+ y: Hex.fromNumber(publicKey2.y),
175
+ }
176
+
177
+ const metadata = {
178
+ credentialId: response.raw.id,
179
+ }
180
+
181
+ const imageHashes = [
182
+ Extensions.Passkeys.rootFor({ ...base1, requireUserVerification: true }),
183
+ Extensions.Passkeys.rootFor({ ...base1, requireUserVerification: false }),
184
+ Extensions.Passkeys.rootFor({ ...base1, requireUserVerification: true, metadata }),
185
+ Extensions.Passkeys.rootFor({ ...base1, requireUserVerification: false, metadata }),
186
+ Extensions.Passkeys.rootFor({ ...base2, requireUserVerification: true }),
187
+ Extensions.Passkeys.rootFor({ ...base2, requireUserVerification: false }),
188
+ Extensions.Passkeys.rootFor({ ...base2, requireUserVerification: true, metadata }),
189
+ Extensions.Passkeys.rootFor({ ...base2, requireUserVerification: false, metadata }),
190
+ ]
191
+
192
+ // Find wallets for all possible image hashes
193
+ const signers = await Promise.all(
194
+ imageHashes.map(async (imageHash) => {
195
+ const wallets = await stateReader.getWalletsForSapient(extensions.passkeys, imageHash)
196
+ return Object.keys(wallets).map((wallet) => ({
197
+ wallet: Address.from(wallet),
198
+ imageHash,
199
+ }))
200
+ }),
201
+ )
202
+
203
+ // Flatten and remove duplicates
204
+ const flattened = signers
205
+ .flat()
206
+ .filter(
207
+ (v, i, self) => self.findIndex((t) => Address.isEqual(t.wallet, v.wallet) && t.imageHash === v.imageHash) === i,
208
+ )
209
+
210
+ // If there are no signers, return undefined
211
+ if (flattened.length === 0) {
212
+ return undefined
213
+ }
214
+
215
+ // If there are multiple signers log a warning
216
+ // but we still return the first one
217
+ if (flattened.length > 1) {
218
+ console.warn('Multiple signers found for passkey', flattened)
219
+ }
220
+
221
+ return Passkey.loadFromWitness(stateReader, extensions, flattened[0]!.wallet, flattened[0]!.imageHash)
222
+ }
223
+
224
+ async signSapient(
225
+ wallet: Address.Address,
226
+ chainId: bigint,
227
+ payload: Payload.Parented,
228
+ imageHash: Hex.Hex,
229
+ ): Promise<SignatureTypes.SignatureOfSapientSignerLeaf> {
230
+ if (this.imageHash !== imageHash) {
231
+ // TODO: This should never get called, why do we have this?
232
+ throw new Error('Unexpected image hash')
233
+ }
234
+
235
+ const challenge = Hex.fromBytes(Payload.hash(wallet, chainId, payload))
236
+
237
+ const response = await WebAuthnP256.sign({
238
+ challenge,
239
+ credentialId: this.credentialId,
240
+ userVerification: this.publicKey.requireUserVerification ? 'required' : 'discouraged',
241
+ })
242
+
243
+ const authenticatorData = Bytes.fromHex(response.metadata.authenticatorData)
244
+ const rBytes = Bytes.fromNumber(response.signature.r)
245
+ const sBytes = Bytes.fromNumber(response.signature.s)
246
+
247
+ const signature = Extensions.Passkeys.encode({
248
+ publicKey: this.publicKey,
249
+ r: rBytes,
250
+ s: sBytes,
251
+ authenticatorData,
252
+ clientDataJSON: response.metadata.clientDataJSON,
253
+ embedMetadata: this.embedMetadata,
254
+ })
255
+
256
+ return {
257
+ address: this.address,
258
+ data: Bytes.toHex(signature),
259
+ type: 'sapient_compact',
260
+ }
261
+ }
262
+
263
+ async witness(stateWriter: State.Writer, wallet: Address.Address, extra?: Object): Promise<void> {
264
+ const payload = Payload.fromMessage(
265
+ Hex.fromString(
266
+ JSON.stringify({
267
+ action: 'consent-to-be-part-of-wallet',
268
+ wallet,
269
+ publicKey: this.publicKey,
270
+ metadata: this.metadata,
271
+ timestamp: Date.now(),
272
+ ...extra,
273
+ } as WitnessMessage),
274
+ ),
275
+ )
276
+
277
+ const signature = await this.signSapient(wallet, 0n, payload, this.imageHash)
278
+ await stateWriter.saveWitnesses(wallet, 0n, payload, {
279
+ type: 'unrecovered-signer',
280
+ weight: 1n,
281
+ signature,
282
+ })
283
+ }
284
+ }
@@ -0,0 +1,153 @@
1
+ import { Hex, Address, PublicKey, Secp256k1, Bytes } from 'ox'
2
+ import { PkStore } from './index.js'
3
+
4
+ export interface EncryptedData {
5
+ iv: Uint8Array
6
+ data: ArrayBuffer
7
+ keyPointer: string
8
+ address: Address.Address
9
+ publicKey: PublicKey.PublicKey
10
+ }
11
+
12
+ export class EncryptedPksDb {
13
+ private tableName: string
14
+ private dbName: string = 'pk-db'
15
+ private dbVersion: number = 1
16
+
17
+ constructor(
18
+ private readonly localStorageKeyPrefix: string = 'e_pk_key_',
19
+ tableName: string = 'e_pk',
20
+ ) {
21
+ this.tableName = tableName
22
+ }
23
+
24
+ private openDB(): Promise<IDBDatabase> {
25
+ return new Promise((resolve, reject) => {
26
+ const request = indexedDB.open(this.dbName, this.dbVersion)
27
+ request.onupgradeneeded = () => {
28
+ const db = request.result
29
+ if (!db.objectStoreNames.contains(this.tableName)) {
30
+ db.createObjectStore(this.tableName)
31
+ }
32
+ }
33
+ request.onsuccess = () => resolve(request.result)
34
+ request.onerror = () => reject(request.error)
35
+ })
36
+ }
37
+
38
+ private async putData(key: string, value: any): Promise<void> {
39
+ const db = await this.openDB()
40
+ return new Promise((resolve, reject) => {
41
+ const tx = db.transaction(this.tableName, 'readwrite')
42
+ const store = tx.objectStore(this.tableName)
43
+ const request = store.put(value, key)
44
+ request.onsuccess = () => resolve()
45
+ request.onerror = () => reject(request.error)
46
+ })
47
+ }
48
+
49
+ private async getData<T>(key: string): Promise<T | undefined> {
50
+ const db = await this.openDB()
51
+ return new Promise((resolve, reject) => {
52
+ const tx = db.transaction(this.tableName, 'readonly')
53
+ const store = tx.objectStore(this.tableName)
54
+ const request = store.get(key)
55
+ request.onsuccess = () => resolve(request.result)
56
+ request.onerror = () => reject(request.error)
57
+ })
58
+ }
59
+
60
+ private async getAllData<T>(): Promise<T[]> {
61
+ const db = await this.openDB()
62
+ return new Promise((resolve, reject) => {
63
+ const tx = db.transaction(this.tableName, 'readonly')
64
+ const store = tx.objectStore(this.tableName)
65
+ const request = store.getAll()
66
+ request.onsuccess = () => resolve(request.result)
67
+ request.onerror = () => reject(request.error)
68
+ })
69
+ }
70
+
71
+ async generateAndStore(): Promise<EncryptedData> {
72
+ const encryptionKey = await window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
73
+ 'encrypt',
74
+ 'decrypt',
75
+ ])
76
+
77
+ const privateKey = Hex.random(32)
78
+
79
+ const publicKey = Secp256k1.getPublicKey({ privateKey })
80
+ const address = Address.fromPublicKey(publicKey)
81
+ const keyPointer = this.localStorageKeyPrefix + address
82
+
83
+ const exportedKey = await window.crypto.subtle.exportKey('jwk', encryptionKey)
84
+ window.localStorage.setItem(keyPointer, JSON.stringify(exportedKey))
85
+
86
+ const encoder = new TextEncoder()
87
+ const encodedPk = encoder.encode(privateKey)
88
+ const iv = window.crypto.getRandomValues(new Uint8Array(12))
89
+ const encryptedBuffer = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv }, encryptionKey, encodedPk)
90
+
91
+ const encrypted: EncryptedData = {
92
+ iv,
93
+ data: encryptedBuffer,
94
+ keyPointer,
95
+ address,
96
+ publicKey,
97
+ }
98
+
99
+ const dbKey = `pk_${address}`
100
+ await this.putData(dbKey, encrypted)
101
+ return encrypted
102
+ }
103
+
104
+ async getEncryptedEntry(address: Address.Address): Promise<EncryptedData | undefined> {
105
+ const dbKey = `pk_${address}`
106
+ return this.getData<EncryptedData>(dbKey)
107
+ }
108
+
109
+ async getEncryptedPkStore(address: Address.Address): Promise<EncryptedPkStore | undefined> {
110
+ const entry = await this.getEncryptedEntry(address)
111
+ if (!entry) return
112
+ return new EncryptedPkStore(entry)
113
+ }
114
+
115
+ async listAddresses(): Promise<Address.Address[]> {
116
+ const allEntries = await this.getAllData<EncryptedData>()
117
+ return allEntries.map((entry) => entry.address)
118
+ }
119
+
120
+ async remove(address: Address.Address) {
121
+ const dbKey = `pk_${address}`
122
+ await this.putData(dbKey, undefined)
123
+ const keyPointer = this.localStorageKeyPrefix + address
124
+ window.localStorage.removeItem(keyPointer)
125
+ }
126
+ }
127
+
128
+ export class EncryptedPkStore implements PkStore {
129
+ constructor(private readonly encrypted: EncryptedData) {}
130
+
131
+ address(): Address.Address {
132
+ return this.encrypted.address
133
+ }
134
+
135
+ publicKey(): PublicKey.PublicKey {
136
+ return this.encrypted.publicKey
137
+ }
138
+
139
+ async signDigest(digest: Bytes.Bytes): Promise<{ r: bigint; s: bigint; yParity: number }> {
140
+ const keyJson = window.localStorage.getItem(this.encrypted.keyPointer)
141
+ if (!keyJson) throw new Error('Encryption key not found in localStorage')
142
+ const jwk = JSON.parse(keyJson)
143
+ const encryptionKey = await window.crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, false, ['decrypt'])
144
+ const decryptedBuffer = await window.crypto.subtle.decrypt(
145
+ { name: 'AES-GCM', iv: this.encrypted.iv },
146
+ encryptionKey,
147
+ this.encrypted.data,
148
+ )
149
+ const decoder = new TextDecoder()
150
+ const privateKey = decoder.decode(decryptedBuffer) as Hex.Hex
151
+ return Secp256k1.sign({ payload: digest, privateKey })
152
+ }
153
+ }