@0xsequence/wallet-wdk 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/.env.test +3 -0
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +202 -0
- package/dist/dbs/auth-commitments.d.ts +17 -0
- package/dist/dbs/auth-commitments.d.ts.map +1 -0
- package/dist/dbs/auth-commitments.js +13 -0
- package/dist/dbs/auth-keys.d.ts +19 -0
- package/dist/dbs/auth-keys.d.ts.map +1 -0
- package/dist/dbs/auth-keys.js +67 -0
- package/dist/dbs/generic.d.ts +33 -0
- package/dist/dbs/generic.d.ts.map +1 -0
- package/dist/dbs/generic.js +170 -0
- package/dist/dbs/index.d.ts +12 -0
- package/dist/dbs/index.d.ts.map +1 -0
- package/dist/dbs/index.js +8 -0
- package/dist/dbs/messages.d.ts +6 -0
- package/dist/dbs/messages.d.ts.map +1 -0
- package/dist/dbs/messages.js +13 -0
- package/dist/dbs/recovery.d.ts +6 -0
- package/dist/dbs/recovery.d.ts.map +1 -0
- package/dist/dbs/recovery.js +13 -0
- package/dist/dbs/signatures.d.ts +6 -0
- package/dist/dbs/signatures.d.ts.map +1 -0
- package/dist/dbs/signatures.js +13 -0
- package/dist/dbs/transactions.d.ts +6 -0
- package/dist/dbs/transactions.d.ts.map +1 -0
- package/dist/dbs/transactions.js +13 -0
- package/dist/dbs/wallets.d.ts +6 -0
- package/dist/dbs/wallets.d.ts.map +1 -0
- package/dist/dbs/wallets.js +13 -0
- package/dist/identity/signer.d.ts +17 -0
- package/dist/identity/signer.d.ts.map +1 -0
- package/dist/identity/signer.js +58 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/sequence/cron.d.ts +19 -0
- package/dist/sequence/cron.d.ts.map +1 -0
- package/dist/sequence/cron.js +118 -0
- package/dist/sequence/devices.d.ts +14 -0
- package/dist/sequence/devices.d.ts.map +1 -0
- package/dist/sequence/devices.js +43 -0
- package/dist/sequence/handlers/authcode-pkce.d.ts +14 -0
- package/dist/sequence/handlers/authcode-pkce.d.ts.map +1 -0
- package/dist/sequence/handlers/authcode-pkce.js +48 -0
- package/dist/sequence/handlers/authcode.d.ts +25 -0
- package/dist/sequence/handlers/authcode.d.ts.map +1 -0
- package/dist/sequence/handlers/authcode.js +91 -0
- package/dist/sequence/handlers/devices.d.ts +14 -0
- package/dist/sequence/handlers/devices.d.ts.map +1 -0
- package/dist/sequence/handlers/devices.js +39 -0
- package/dist/sequence/handlers/handler.d.ts +8 -0
- package/dist/sequence/handlers/handler.d.ts.map +1 -0
- package/dist/sequence/handlers/handler.js +1 -0
- package/dist/sequence/handlers/identity.d.ts +21 -0
- package/dist/sequence/handlers/identity.d.ts.map +1 -0
- package/dist/sequence/handlers/identity.js +86 -0
- package/dist/sequence/handlers/index.d.ts +7 -0
- package/dist/sequence/handlers/index.d.ts.map +1 -0
- package/dist/sequence/handlers/index.js +5 -0
- package/dist/sequence/handlers/mnemonic.d.ts +19 -0
- package/dist/sequence/handlers/mnemonic.d.ts.map +1 -0
- package/dist/sequence/handlers/mnemonic.js +67 -0
- package/dist/sequence/handlers/otp.d.ts +20 -0
- package/dist/sequence/handlers/otp.d.ts.map +1 -0
- package/dist/sequence/handlers/otp.js +83 -0
- package/dist/sequence/handlers/passkeys.d.ts +17 -0
- package/dist/sequence/handlers/passkeys.d.ts.map +1 -0
- package/dist/sequence/handlers/passkeys.js +63 -0
- package/dist/sequence/handlers/recovery.d.ts +15 -0
- package/dist/sequence/handlers/recovery.d.ts.map +1 -0
- package/dist/sequence/handlers/recovery.js +72 -0
- package/dist/sequence/index.d.ts +12 -0
- package/dist/sequence/index.d.ts.map +1 -0
- package/dist/sequence/index.js +9 -0
- package/dist/sequence/logger.d.ts +7 -0
- package/dist/sequence/logger.d.ts.map +1 -0
- package/dist/sequence/logger.js +11 -0
- package/dist/sequence/manager.d.ts +287 -0
- package/dist/sequence/manager.d.ts.map +1 -0
- package/dist/sequence/manager.js +356 -0
- package/dist/sequence/messages.d.ts +18 -0
- package/dist/sequence/messages.d.ts.map +1 -0
- package/dist/sequence/messages.js +115 -0
- package/dist/sequence/recovery.d.ts +30 -0
- package/dist/sequence/recovery.d.ts.map +1 -0
- package/dist/sequence/recovery.js +314 -0
- package/dist/sequence/sessions.d.ts +26 -0
- package/dist/sequence/sessions.d.ts.map +1 -0
- package/dist/sequence/sessions.js +169 -0
- package/dist/sequence/signatures.d.ts +21 -0
- package/dist/sequence/signatures.d.ts.map +1 -0
- package/dist/sequence/signatures.js +192 -0
- package/dist/sequence/signers.d.ts +14 -0
- package/dist/sequence/signers.d.ts.map +1 -0
- package/dist/sequence/signers.js +74 -0
- package/dist/sequence/transactions.d.ts +26 -0
- package/dist/sequence/transactions.d.ts.map +1 -0
- package/dist/sequence/transactions.js +201 -0
- package/dist/sequence/types/index.d.ts +9 -0
- package/dist/sequence/types/index.d.ts.map +1 -0
- package/dist/sequence/types/index.js +2 -0
- package/dist/sequence/types/message-request.d.ts +23 -0
- package/dist/sequence/types/message-request.d.ts.map +1 -0
- package/dist/sequence/types/message-request.js +1 -0
- package/dist/sequence/types/recovery.d.ts +15 -0
- package/dist/sequence/types/recovery.d.ts.map +1 -0
- package/dist/sequence/types/recovery.js +1 -0
- package/dist/sequence/types/signature-request.d.ts +76 -0
- package/dist/sequence/types/signature-request.d.ts.map +1 -0
- package/dist/sequence/types/signature-request.js +11 -0
- package/dist/sequence/types/signer.d.ts +28 -0
- package/dist/sequence/types/signer.d.ts.map +1 -0
- package/dist/sequence/types/signer.js +10 -0
- package/dist/sequence/types/transaction-request.d.ts +41 -0
- package/dist/sequence/types/transaction-request.d.ts.map +1 -0
- package/dist/sequence/types/transaction-request.js +1 -0
- package/dist/sequence/types/wallet.d.ts +21 -0
- package/dist/sequence/types/wallet.d.ts.map +1 -0
- package/dist/sequence/types/wallet.js +1 -0
- package/dist/sequence/wallets.d.ts +121 -0
- package/dist/sequence/wallets.d.ts.map +1 -0
- package/dist/sequence/wallets.js +632 -0
- package/package.json +40 -0
- package/src/dbs/auth-commitments.ts +26 -0
- package/src/dbs/auth-keys.ts +85 -0
- package/src/dbs/generic.ts +194 -0
- package/src/dbs/index.ts +13 -0
- package/src/dbs/messages.ts +16 -0
- package/src/dbs/recovery.ts +15 -0
- package/src/dbs/signatures.ts +15 -0
- package/src/dbs/transactions.ts +16 -0
- package/src/dbs/wallets.ts +16 -0
- package/src/identity/signer.ts +78 -0
- package/src/index.ts +2 -0
- package/src/sequence/cron.ts +134 -0
- package/src/sequence/devices.ts +53 -0
- package/src/sequence/handlers/authcode-pkce.ts +70 -0
- package/src/sequence/handlers/authcode.ts +116 -0
- package/src/sequence/handlers/devices.ts +53 -0
- package/src/sequence/handlers/handler.ts +14 -0
- package/src/sequence/handlers/identity.ts +101 -0
- package/src/sequence/handlers/index.ts +6 -0
- package/src/sequence/handlers/mnemonic.ts +88 -0
- package/src/sequence/handlers/otp.ts +107 -0
- package/src/sequence/handlers/passkeys.ts +84 -0
- package/src/sequence/handlers/recovery.ts +88 -0
- package/src/sequence/index.ts +25 -0
- package/src/sequence/logger.ts +11 -0
- package/src/sequence/manager.ts +634 -0
- package/src/sequence/messages.ts +146 -0
- package/src/sequence/recovery.ts +429 -0
- package/src/sequence/sessions.ts +238 -0
- package/src/sequence/signatures.ts +263 -0
- package/src/sequence/signers.ts +88 -0
- package/src/sequence/transactions.ts +281 -0
- package/src/sequence/types/index.ts +27 -0
- package/src/sequence/types/message-request.ts +26 -0
- package/src/sequence/types/recovery.ts +15 -0
- package/src/sequence/types/signature-request.ts +89 -0
- package/src/sequence/types/signer.ts +32 -0
- package/src/sequence/types/transaction-request.ts +47 -0
- package/src/sequence/types/wallet.ts +24 -0
- package/src/sequence/wallets.ts +853 -0
- package/test/constants.ts +62 -0
- package/test/recovery.test.ts +211 -0
- package/test/sessions.test.ts +324 -0
- package/test/setup.ts +63 -0
- package/test/transactions.test.ts +464 -0
- package/test/wallets.test.ts +381 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Envelope, Wallet } from '@0xsequence/wallet-core'
|
|
2
|
+
import { Payload } from '@0xsequence/wallet-primitives'
|
|
3
|
+
import { Address, Bytes, Hex, Provider, RpcTransport } from 'ox'
|
|
4
|
+
import { v7 as uuidv7 } from 'uuid'
|
|
5
|
+
import { Shared } from './manager.js'
|
|
6
|
+
import { Message, MessageRequest, MessageRequested, MessageSigned } from './types/message-request.js'
|
|
7
|
+
|
|
8
|
+
export class Messages {
|
|
9
|
+
constructor(private readonly shared: Shared) {}
|
|
10
|
+
|
|
11
|
+
public async list(): Promise<Message[]> {
|
|
12
|
+
return this.shared.databases.messages.list()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async get(messageOrSignatureId: string): Promise<Message> {
|
|
16
|
+
return this.getByMessageOrSignatureId(messageOrSignatureId)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async getByMessageOrSignatureId(messageOrSignatureId: string): Promise<Message> {
|
|
20
|
+
const messages = await this.list()
|
|
21
|
+
const message = messages.find((m) => m.id === messageOrSignatureId || m.signatureId === messageOrSignatureId)
|
|
22
|
+
if (!message) {
|
|
23
|
+
throw new Error(`Message ${messageOrSignatureId} not found`)
|
|
24
|
+
}
|
|
25
|
+
return message
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async request(
|
|
29
|
+
from: Address.Address,
|
|
30
|
+
message: MessageRequest,
|
|
31
|
+
chainId?: bigint,
|
|
32
|
+
options?: {
|
|
33
|
+
source?: string
|
|
34
|
+
},
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
const wallet = new Wallet(from, { stateProvider: this.shared.sequence.stateProvider })
|
|
37
|
+
|
|
38
|
+
// Prepare message payload
|
|
39
|
+
const envelope = await wallet.prepareMessageSignature(message, chainId ?? 0n)
|
|
40
|
+
|
|
41
|
+
// Prepare signature request
|
|
42
|
+
const signatureRequest = await this.shared.modules.signatures.request(envelope, 'sign-message', {
|
|
43
|
+
origin: options?.source,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const id = uuidv7()
|
|
47
|
+
await this.shared.databases.messages.set({
|
|
48
|
+
id,
|
|
49
|
+
wallet: from,
|
|
50
|
+
message,
|
|
51
|
+
envelope,
|
|
52
|
+
source: options?.source ?? 'unknown',
|
|
53
|
+
status: 'requested',
|
|
54
|
+
signatureId: signatureRequest,
|
|
55
|
+
} as MessageRequested)
|
|
56
|
+
|
|
57
|
+
return signatureRequest
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async complete(messageOrSignatureId: string): Promise<string> {
|
|
61
|
+
const message = await this.getByMessageOrSignatureId(messageOrSignatureId)
|
|
62
|
+
|
|
63
|
+
if (message.status === 'signed') {
|
|
64
|
+
// Return the message signature
|
|
65
|
+
return message.messageSignature
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const messageId = message.id
|
|
69
|
+
const signature = await this.shared.modules.signatures.get(message.signatureId)
|
|
70
|
+
if (!signature) {
|
|
71
|
+
throw new Error(`Signature ${message.signatureId} not found for message ${messageId}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!Payload.isMessage(message.envelope.payload) || !Payload.isMessage(signature.envelope.payload)) {
|
|
75
|
+
throw new Error(`Message ${messageId} is not a message payload`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!Envelope.isSigned(signature.envelope)) {
|
|
79
|
+
throw new Error(`Message ${messageId} is not signed`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const signatureEnvelope = signature.envelope as Envelope.Signed<Payload.Message>
|
|
83
|
+
const { weight, threshold } = Envelope.weightOf(signatureEnvelope)
|
|
84
|
+
if (weight < threshold) {
|
|
85
|
+
throw new Error(`Message ${messageId} has insufficient weight`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get the provider for the message chain
|
|
89
|
+
let provider: Provider.Provider | undefined
|
|
90
|
+
if (message.envelope.chainId !== 0n) {
|
|
91
|
+
const network = this.shared.sequence.networks.find((network) => network.chainId === message.envelope.chainId)
|
|
92
|
+
if (!network) {
|
|
93
|
+
throw new Error(`Network not found for ${message.envelope.chainId}`)
|
|
94
|
+
}
|
|
95
|
+
const transport = RpcTransport.fromHttp(network.rpc)
|
|
96
|
+
provider = Provider.from(transport)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const wallet = new Wallet(message.wallet, { stateProvider: this.shared.sequence.stateProvider })
|
|
100
|
+
const messageSignature = Hex.from(await wallet.buildMessageSignature(signatureEnvelope, provider))
|
|
101
|
+
|
|
102
|
+
await this.shared.databases.messages.set({
|
|
103
|
+
...message,
|
|
104
|
+
envelope: signature.envelope,
|
|
105
|
+
status: 'signed',
|
|
106
|
+
messageSignature,
|
|
107
|
+
} as MessageSigned)
|
|
108
|
+
await this.shared.modules.signatures.complete(signature.id)
|
|
109
|
+
|
|
110
|
+
return messageSignature
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onMessagesUpdate(cb: (messages: Message[]) => void, trigger?: boolean) {
|
|
114
|
+
const undo = this.shared.databases.messages.addListener(() => {
|
|
115
|
+
this.list().then((l) => cb(l))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (trigger) {
|
|
119
|
+
this.list().then((l) => cb(l))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return undo
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onMessageUpdate(messageId: string, cb: (message: Message) => void, trigger?: boolean) {
|
|
126
|
+
const undo = this.shared.databases.messages.addListener(() => {
|
|
127
|
+
this.get(messageId).then((t) => cb(t))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (trigger) {
|
|
131
|
+
this.get(messageId).then((t) => cb(t))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return undo
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async delete(messageOrSignatureId: string) {
|
|
138
|
+
try {
|
|
139
|
+
const message = await this.getByMessageOrSignatureId(messageOrSignatureId)
|
|
140
|
+
await this.shared.databases.signatures.del(message.signatureId)
|
|
141
|
+
await this.shared.databases.messages.del(message.id)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Ignore
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { Config, Extensions, GenericTree, Payload } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { Shared } from './manager.js'
|
|
3
|
+
import { Address, Hex, Provider, RpcTransport } from 'ox'
|
|
4
|
+
import { Kinds, RecoverySigner } from './types/signer.js'
|
|
5
|
+
import { Envelope } from '@0xsequence/wallet-core'
|
|
6
|
+
import { QueuedRecoveryPayload } from './types/recovery.js'
|
|
7
|
+
import { Actions } from './types/index.js'
|
|
8
|
+
import { MnemonicHandler } from './handlers/mnemonic.js'
|
|
9
|
+
|
|
10
|
+
export class Recovery {
|
|
11
|
+
constructor(private readonly shared: Shared) {}
|
|
12
|
+
|
|
13
|
+
initialize() {
|
|
14
|
+
this.shared.modules.cron.registerJob(
|
|
15
|
+
'update-queued-recovery-payloads',
|
|
16
|
+
5 * 60 * 1000, // 5 minutes
|
|
17
|
+
async () => {
|
|
18
|
+
this.shared.modules.logger.log('Running job: update-queued-recovery-payloads')
|
|
19
|
+
await this.updateQueuedRecoveryPayloads()
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
this.shared.modules.logger.log('Recovery module initialized and job registered.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async updateRecoveryModule(
|
|
26
|
+
modules: Config.SapientSignerLeaf[],
|
|
27
|
+
transformer: (leaves: Extensions.Recovery.RecoveryLeaf[]) => Extensions.Recovery.RecoveryLeaf[],
|
|
28
|
+
) {
|
|
29
|
+
const ext = this.shared.sequence.extensions.recovery
|
|
30
|
+
const idx = modules.findIndex((m) => m.address === ext)
|
|
31
|
+
if (idx === -1) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sapientSigner = modules[idx]
|
|
36
|
+
if (!sapientSigner) {
|
|
37
|
+
throw new Error('recovery-module-not-found')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const genericTree = await this.shared.sequence.stateProvider.getTree(sapientSigner.imageHash)
|
|
41
|
+
if (!genericTree) {
|
|
42
|
+
throw new Error('recovery-module-tree-not-found')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tree = Extensions.Recovery.fromGenericTree(genericTree)
|
|
46
|
+
const { leaves, isComplete } = Extensions.Recovery.getRecoveryLeaves(tree)
|
|
47
|
+
if (!isComplete) {
|
|
48
|
+
throw new Error('recovery-module-tree-incomplete')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const nextTree = Extensions.Recovery.fromRecoveryLeaves(transformer(leaves))
|
|
52
|
+
const nextGeneric = Extensions.Recovery.toGenericTree(nextTree)
|
|
53
|
+
await this.shared.sequence.stateProvider.saveTree(nextGeneric)
|
|
54
|
+
if (!modules[idx]) {
|
|
55
|
+
throw new Error('recovery-module-not-found-(unreachable)')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
modules[idx].imageHash = GenericTree.hash(nextGeneric)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async initRecoveryModule(modules: Config.SapientSignerLeaf[], address: Address.Address) {
|
|
62
|
+
if (this.hasRecoveryModule(modules)) {
|
|
63
|
+
throw new Error('recovery-module-already-initialized')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const recoveryTree = Extensions.Recovery.fromRecoveryLeaves([
|
|
67
|
+
{
|
|
68
|
+
type: 'leaf' as const,
|
|
69
|
+
signer: address,
|
|
70
|
+
requiredDeltaTime: this.shared.sequence.defaultRecoverySettings.requiredDeltaTime,
|
|
71
|
+
minTimestamp: this.shared.sequence.defaultRecoverySettings.minTimestamp,
|
|
72
|
+
},
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
const recoveryGenericTree = Extensions.Recovery.toGenericTree(recoveryTree)
|
|
76
|
+
await this.shared.sequence.stateProvider.saveTree(recoveryGenericTree)
|
|
77
|
+
|
|
78
|
+
const recoveryImageHash = GenericTree.hash(recoveryGenericTree)
|
|
79
|
+
|
|
80
|
+
modules.push({
|
|
81
|
+
type: 'sapient-signer',
|
|
82
|
+
address: this.shared.sequence.extensions.recovery,
|
|
83
|
+
weight: 255n,
|
|
84
|
+
imageHash: recoveryImageHash,
|
|
85
|
+
} as Config.SapientSignerLeaf)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hasRecoveryModule(modules: Config.SapientSignerLeaf[]): boolean {
|
|
89
|
+
return modules.some((m) => Address.isEqual(m.address, this.shared.sequence.extensions.recovery))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async addRecoverySignerToModules(modules: Config.SapientSignerLeaf[], address: Address.Address) {
|
|
93
|
+
if (!this.hasRecoveryModule(modules)) {
|
|
94
|
+
throw new Error('recovery-module-not-enabled')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await this.updateRecoveryModule(modules, (leaves) => {
|
|
98
|
+
if (leaves.some((l) => Address.isEqual(l.signer, address))) {
|
|
99
|
+
return leaves
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const filtered = leaves.filter((l) => !Address.isEqual(l.signer, '0x0000000000000000000000000000000000000000'))
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
...filtered,
|
|
106
|
+
{
|
|
107
|
+
type: 'leaf',
|
|
108
|
+
signer: address,
|
|
109
|
+
requiredDeltaTime: this.shared.sequence.defaultRecoverySettings.requiredDeltaTime,
|
|
110
|
+
minTimestamp: this.shared.sequence.defaultRecoverySettings.minTimestamp,
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async removeRecoverySignerFromModules(modules: Config.SapientSignerLeaf[], address: Address.Address) {
|
|
117
|
+
if (!this.hasRecoveryModule(modules)) {
|
|
118
|
+
throw new Error('recovery-module-not-enabled')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await this.updateRecoveryModule(modules, (leaves) => {
|
|
122
|
+
const next = leaves.filter((l) => l.signer !== address)
|
|
123
|
+
if (next.length === 0) {
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
type: 'leaf',
|
|
127
|
+
signer: '0x0000000000000000000000000000000000000000',
|
|
128
|
+
requiredDeltaTime: 0n,
|
|
129
|
+
minTimestamp: 0n,
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return next
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async addRecoveryMnemonic(wallet: Address.Address, mnemonic: string) {
|
|
139
|
+
const signer = MnemonicHandler.toSigner(mnemonic)
|
|
140
|
+
if (!signer) {
|
|
141
|
+
throw new Error('invalid-mnemonic')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await signer.witness(this.shared.sequence.stateProvider, wallet, {
|
|
145
|
+
isForRecovery: true,
|
|
146
|
+
signerKind: Kinds.LoginMnemonic,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return this.addRecoverySigner(wallet, signer.address)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async addRecoverySigner(wallet: Address.Address, address: Address.Address) {
|
|
153
|
+
const { modules } = await this.shared.modules.wallets.getConfigurationParts(wallet)
|
|
154
|
+
await this.addRecoverySignerToModules(modules, address)
|
|
155
|
+
return this.shared.modules.wallets.requestConfigurationUpdate(
|
|
156
|
+
wallet,
|
|
157
|
+
{
|
|
158
|
+
modules,
|
|
159
|
+
},
|
|
160
|
+
Actions.AddRecoverySigner,
|
|
161
|
+
'wallet-webapp',
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async removeRecoverySigner(wallet: Address.Address, address: Address.Address) {
|
|
166
|
+
const { modules } = await this.shared.modules.wallets.getConfigurationParts(wallet)
|
|
167
|
+
await this.removeRecoverySignerFromModules(modules, address)
|
|
168
|
+
return this.shared.modules.wallets.requestConfigurationUpdate(
|
|
169
|
+
wallet,
|
|
170
|
+
{ modules },
|
|
171
|
+
Actions.RemoveRecoverySigner,
|
|
172
|
+
'wallet-webapp',
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async completeRecoveryUpdate(requestId: string) {
|
|
177
|
+
const request = await this.shared.modules.signatures.get(requestId)
|
|
178
|
+
if (request.action !== 'add-recovery-signer' && request.action !== 'remove-recovery-signer') {
|
|
179
|
+
throw new Error('invalid-recovery-update-action')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return this.shared.modules.wallets.completeConfigurationUpdate(requestId)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getRecoverySigners(address: Address.Address): Promise<RecoverySigner[] | undefined> {
|
|
186
|
+
const { raw } = await this.shared.modules.wallets.getConfiguration(address)
|
|
187
|
+
const recoveryLeaf = raw.modules.find((m) => Address.isEqual(m.address, this.shared.sequence.extensions.recovery))
|
|
188
|
+
if (!recoveryLeaf) {
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const recoveryGenericTree = await this.shared.sequence.stateProvider.getTree(recoveryLeaf.imageHash)
|
|
193
|
+
if (!recoveryGenericTree) {
|
|
194
|
+
throw new Error('recovery-module-tree-not-found')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const recoveryTree = Extensions.Recovery.fromGenericTree(recoveryGenericTree)
|
|
198
|
+
const { leaves, isComplete } = Extensions.Recovery.getRecoveryLeaves(recoveryTree)
|
|
199
|
+
if (!isComplete) {
|
|
200
|
+
throw new Error('recovery-module-tree-incomplete')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const kos = await this.shared.modules.signers.resolveKinds(
|
|
204
|
+
address,
|
|
205
|
+
leaves.map((l) => l.signer),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return leaves
|
|
209
|
+
.filter((l) => !Address.isEqual(l.signer, '0x0000000000000000000000000000000000000000'))
|
|
210
|
+
.map((l) => ({
|
|
211
|
+
address: l.signer,
|
|
212
|
+
kind: kos.find((s) => Address.isEqual(s.address, l.signer))?.kind || 'unknown',
|
|
213
|
+
isRecovery: true,
|
|
214
|
+
minTimestamp: l.minTimestamp,
|
|
215
|
+
requiredDeltaTime: l.requiredDeltaTime,
|
|
216
|
+
}))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async queueRecoveryPayload(wallet: Address.Address, chainId: bigint, payload: Payload.Calls) {
|
|
220
|
+
const signers = await this.getRecoverySigners(wallet)
|
|
221
|
+
if (!signers) {
|
|
222
|
+
throw new Error('recovery-signers-not-found')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const recoveryPayload = Payload.toRecovery(payload)
|
|
226
|
+
const simulatedTopology = Config.flatLeavesToTopology(
|
|
227
|
+
signers.map((s) => ({
|
|
228
|
+
type: 'signer',
|
|
229
|
+
address: s.address,
|
|
230
|
+
weight: 1n,
|
|
231
|
+
})),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Save both versions of the payload in parallel
|
|
235
|
+
await Promise.all([
|
|
236
|
+
this.shared.sequence.stateProvider.savePayload(wallet, payload, chainId),
|
|
237
|
+
this.shared.sequence.stateProvider.savePayload(wallet, recoveryPayload, chainId),
|
|
238
|
+
])
|
|
239
|
+
|
|
240
|
+
const requestId = await this.shared.modules.signatures.request(
|
|
241
|
+
{
|
|
242
|
+
wallet,
|
|
243
|
+
chainId,
|
|
244
|
+
configuration: {
|
|
245
|
+
threshold: 1n,
|
|
246
|
+
checkpoint: 0n,
|
|
247
|
+
topology: simulatedTopology,
|
|
248
|
+
},
|
|
249
|
+
payload: recoveryPayload,
|
|
250
|
+
},
|
|
251
|
+
'recovery',
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return requestId
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// TODO: Handle this transaction instead of just returning the to and data
|
|
258
|
+
async completeRecoveryPayload(requestId: string): Promise<{ to: Address.Address; data: Hex.Hex }> {
|
|
259
|
+
const signature = await this.shared.modules.signatures.get(requestId)
|
|
260
|
+
if (signature.action !== 'recovery' || !Payload.isRecovery(signature.envelope.payload)) {
|
|
261
|
+
throw new Error('invalid-recovery-payload')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!Envelope.isSigned(signature.envelope)) {
|
|
265
|
+
throw new Error('recovery-payload-not-signed')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const { weight, threshold } = Envelope.weightOf(signature.envelope)
|
|
269
|
+
if (weight < threshold) {
|
|
270
|
+
throw new Error('recovery-payload-insufficient-weight')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find any valid signature
|
|
274
|
+
const validSignature = signature.envelope.signatures[0]
|
|
275
|
+
if (Envelope.isSapientSignature(validSignature)) {
|
|
276
|
+
throw new Error('recovery-payload-sapient-signatures-not-supported')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!validSignature) {
|
|
280
|
+
throw new Error('recovery-payload-no-valid-signature')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const calldata = Extensions.Recovery.encodeCalldata(
|
|
284
|
+
signature.wallet,
|
|
285
|
+
signature.envelope.payload,
|
|
286
|
+
validSignature.address,
|
|
287
|
+
validSignature.signature,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
to: this.shared.sequence.extensions.recovery,
|
|
292
|
+
data: calldata,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async getQueuedRecoveryPayloads(wallet?: Address.Address): Promise<QueuedRecoveryPayload[]> {
|
|
297
|
+
const all = await this.shared.databases.recovery.list()
|
|
298
|
+
if (wallet) {
|
|
299
|
+
return all.filter((p) => Address.isEqual(p.wallet, wallet))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return all
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
onQueuedRecoveryPayloadsUpdate(
|
|
306
|
+
wallet: Address.Address | undefined,
|
|
307
|
+
cb: (payloads: QueuedRecoveryPayload[]) => void,
|
|
308
|
+
trigger?: boolean,
|
|
309
|
+
) {
|
|
310
|
+
if (trigger) {
|
|
311
|
+
this.getQueuedRecoveryPayloads(wallet).then(cb)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return this.shared.databases.recovery.addListener(() => {
|
|
315
|
+
this.getQueuedRecoveryPayloads(wallet).then(cb)
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async updateQueuedRecoveryPayloads(): Promise<void> {
|
|
320
|
+
const wallets = await this.shared.modules.wallets.list()
|
|
321
|
+
if (wallets.length === 0) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Create providers for each network
|
|
326
|
+
const providers = this.shared.sequence.networks.map((network) => ({
|
|
327
|
+
chainId: network.chainId,
|
|
328
|
+
provider: Provider.from(RpcTransport.fromHttp(network.rpc)),
|
|
329
|
+
}))
|
|
330
|
+
|
|
331
|
+
const seenInThisRun = new Set<string>()
|
|
332
|
+
|
|
333
|
+
for (const wallet of wallets) {
|
|
334
|
+
// See if they have any recover signers
|
|
335
|
+
const signers = await this.getRecoverySigners(wallet.address)
|
|
336
|
+
if (!signers || signers.length === 0) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Now we need to fetch, for each signer and network, any queued recovery payloads
|
|
341
|
+
// TODO: This may benefit from multicall, but it is not urgent, as this happens in the background
|
|
342
|
+
for (const signer of signers) {
|
|
343
|
+
for (const { chainId, provider } of providers) {
|
|
344
|
+
const totalPayloads = await Extensions.Recovery.totalQueuedPayloads(
|
|
345
|
+
provider,
|
|
346
|
+
this.shared.sequence.extensions.recovery,
|
|
347
|
+
wallet.address,
|
|
348
|
+
signer.address,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
for (let i = 0n; i < totalPayloads; i++) {
|
|
352
|
+
const payloadHash = await Extensions.Recovery.queuedPayloadHashOf(
|
|
353
|
+
provider,
|
|
354
|
+
this.shared.sequence.extensions.recovery,
|
|
355
|
+
wallet.address,
|
|
356
|
+
signer.address,
|
|
357
|
+
i,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
const timestamp = await Extensions.Recovery.timestampForQueuedPayload(
|
|
361
|
+
provider,
|
|
362
|
+
this.shared.sequence.extensions.recovery,
|
|
363
|
+
wallet.address,
|
|
364
|
+
signer.address,
|
|
365
|
+
payloadHash,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const payload = await this.shared.sequence.stateProvider.getPayload(payloadHash)
|
|
369
|
+
|
|
370
|
+
// If ready, we need to check if it was executed already
|
|
371
|
+
// for this, we check if the wallet 77nonce for the given space
|
|
372
|
+
// is greater than the nonce in the payload
|
|
373
|
+
if (timestamp < Date.now() / 1000 && payload && Payload.isCalls(payload.payload)) {
|
|
374
|
+
const nonce = await this.shared.modules.wallets.getNonce(chainId, wallet.address, payload.payload.space)
|
|
375
|
+
if (nonce > i) {
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// The id is the index + signer address + chainId + wallet address
|
|
381
|
+
const id = `${i}-${signer.address}-${chainId}-${wallet.address}`
|
|
382
|
+
|
|
383
|
+
// Create a new payload
|
|
384
|
+
const payloadEntry: QueuedRecoveryPayload = {
|
|
385
|
+
id,
|
|
386
|
+
index: i,
|
|
387
|
+
recoveryModule: this.shared.sequence.extensions.recovery,
|
|
388
|
+
wallet: wallet.address,
|
|
389
|
+
signer: signer.address,
|
|
390
|
+
chainId,
|
|
391
|
+
startTimestamp: timestamp,
|
|
392
|
+
endTimestamp: timestamp + signer.requiredDeltaTime,
|
|
393
|
+
payloadHash,
|
|
394
|
+
payload: payload?.payload,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await this.shared.databases.recovery.set(payloadEntry)
|
|
398
|
+
seenInThisRun.add(payloadEntry.id)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Delete any unseen queued payloads as they are no longer relevant
|
|
404
|
+
const allQueuedPayloads = await this.shared.databases.recovery.list()
|
|
405
|
+
for (const payload of allQueuedPayloads) {
|
|
406
|
+
if (!seenInThisRun.has(payload.id)) {
|
|
407
|
+
await this.shared.databases.recovery.del(payload.id)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async encodeRecoverySignature(imageHash: Hex.Hex, signer: Address.Address) {
|
|
414
|
+
const genericTree = await this.shared.sequence.stateProvider.getTree(imageHash)
|
|
415
|
+
if (!genericTree) {
|
|
416
|
+
throw new Error('recovery-module-tree-not-found')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const tree = Extensions.Recovery.fromGenericTree(genericTree)
|
|
420
|
+
const allSigners = Extensions.Recovery.getRecoveryLeaves(tree).leaves.map((l) => l.signer)
|
|
421
|
+
|
|
422
|
+
if (!allSigners.includes(signer)) {
|
|
423
|
+
throw new Error('signer-not-found-in-recovery-module')
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const trimmed = Extensions.Recovery.trimTopology(tree, signer)
|
|
427
|
+
return Extensions.Recovery.encodeTopology(trimmed)
|
|
428
|
+
}
|
|
429
|
+
}
|