@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.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +202 -0
- package/dist/envelope.d.ts +34 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +96 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/relayer/index.d.ts +4 -0
- package/dist/relayer/index.d.ts.map +1 -0
- package/dist/relayer/index.js +3 -0
- package/dist/relayer/local.d.ts +28 -0
- package/dist/relayer/local.d.ts.map +1 -0
- package/dist/relayer/local.js +101 -0
- package/dist/relayer/pk-relayer.d.ts +18 -0
- package/dist/relayer/pk-relayer.d.ts.map +1 -0
- package/dist/relayer/pk-relayer.js +88 -0
- package/dist/relayer/relayer.d.ts +39 -0
- package/dist/relayer/relayer.d.ts.map +1 -0
- package/dist/relayer/relayer.js +1 -0
- package/dist/signers/index.d.ts +23 -0
- package/dist/signers/index.d.ts.map +1 -0
- package/dist/signers/index.js +10 -0
- package/dist/signers/passkey.d.ts +41 -0
- package/dist/signers/passkey.d.ts.map +1 -0
- package/dist/signers/passkey.js +196 -0
- package/dist/signers/pk/encrypted.d.ts +37 -0
- package/dist/signers/pk/encrypted.d.ts.map +1 -0
- package/dist/signers/pk/encrypted.js +123 -0
- package/dist/signers/pk/index.d.ts +35 -0
- package/dist/signers/pk/index.d.ts.map +1 -0
- package/dist/signers/pk/index.js +51 -0
- package/dist/signers/session/explicit.d.ts +18 -0
- package/dist/signers/session/explicit.d.ts.map +1 -0
- package/dist/signers/session/explicit.js +126 -0
- package/dist/signers/session/implicit.d.ts +20 -0
- package/dist/signers/session/implicit.d.ts.map +1 -0
- package/dist/signers/session/implicit.js +120 -0
- package/dist/signers/session/index.d.ts +4 -0
- package/dist/signers/session/index.d.ts.map +1 -0
- package/dist/signers/session/index.js +3 -0
- package/dist/signers/session/session.d.ts +11 -0
- package/dist/signers/session/session.d.ts.map +1 -0
- package/dist/signers/session/session.js +1 -0
- package/dist/signers/session-manager.d.ts +33 -0
- package/dist/signers/session-manager.d.ts.map +1 -0
- package/dist/signers/session-manager.js +181 -0
- package/dist/state/cached.d.ts +59 -0
- package/dist/state/cached.d.ts.map +1 -0
- package/dist/state/cached.js +157 -0
- package/dist/state/index.d.ts +61 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +4 -0
- package/dist/state/local/index.d.ts +98 -0
- package/dist/state/local/index.d.ts.map +1 -0
- package/dist/state/local/index.js +247 -0
- package/dist/state/local/indexed-db.d.ts +41 -0
- package/dist/state/local/indexed-db.d.ts.map +1 -0
- package/dist/state/local/indexed-db.js +149 -0
- package/dist/state/local/memory.d.ts +41 -0
- package/dist/state/local/memory.d.ts.map +1 -0
- package/dist/state/local/memory.js +77 -0
- package/dist/state/remote/dev-http.d.ts +57 -0
- package/dist/state/remote/dev-http.d.ts.map +1 -0
- package/dist/state/remote/dev-http.js +162 -0
- package/dist/state/remote/index.d.ts +2 -0
- package/dist/state/remote/index.d.ts.map +1 -0
- package/dist/state/remote/index.js +1 -0
- package/dist/state/utils.d.ts +12 -0
- package/dist/state/utils.d.ts.map +1 -0
- package/dist/state/utils.js +29 -0
- package/dist/wallet.d.ts +58 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +306 -0
- package/package.json +33 -0
- package/src/envelope.ts +148 -0
- package/src/index.ts +6 -0
- package/src/relayer/index.ts +3 -0
- package/src/relayer/local.ts +125 -0
- package/src/relayer/pk-relayer.ts +110 -0
- package/src/relayer/relayer.ts +52 -0
- package/src/signers/index.ts +44 -0
- package/src/signers/passkey.ts +284 -0
- package/src/signers/pk/encrypted.ts +153 -0
- package/src/signers/pk/index.ts +77 -0
- package/src/signers/session/explicit.ts +173 -0
- package/src/signers/session/implicit.ts +145 -0
- package/src/signers/session/index.ts +3 -0
- package/src/signers/session/session.ts +26 -0
- package/src/signers/session-manager.ts +241 -0
- package/src/state/cached.ts +233 -0
- package/src/state/index.ts +85 -0
- package/src/state/local/index.ts +422 -0
- package/src/state/local/indexed-db.ts +204 -0
- package/src/state/local/memory.ts +126 -0
- package/src/state/remote/dev-http.ts +253 -0
- package/src/state/remote/index.ts +1 -0
- package/src/state/utils.ts +50 -0
- package/src/wallet.ts +390 -0
- package/test/constants.ts +15 -0
- package/test/session-manager.test.ts +451 -0
- package/test/setup.ts +63 -0
- package/test/wallet.test.ts +90 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|