@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,238 @@
|
|
|
1
|
+
import { Signers as CoreSigners, Envelope } from '@0xsequence/wallet-core'
|
|
2
|
+
import {
|
|
3
|
+
Attestation,
|
|
4
|
+
Config,
|
|
5
|
+
Constants,
|
|
6
|
+
GenericTree,
|
|
7
|
+
Payload,
|
|
8
|
+
Signature as SequenceSignature,
|
|
9
|
+
SessionConfig,
|
|
10
|
+
} from '@0xsequence/wallet-primitives'
|
|
11
|
+
import { Address, Bytes, Hash, Hex } from 'ox'
|
|
12
|
+
import { IdentityType } from '@0xsequence/identity-instrument'
|
|
13
|
+
import { AuthCodePkceHandler } from './handlers/authcode-pkce.js'
|
|
14
|
+
import { IdentityHandler, identityTypeToHex } from './handlers/identity.js'
|
|
15
|
+
import { ManagerOptionsDefaults, Shared } from './manager.js'
|
|
16
|
+
import { Actions } from './types/signature-request.js'
|
|
17
|
+
|
|
18
|
+
export type AuthorizeImplicitSessionArgs = {
|
|
19
|
+
target: string
|
|
20
|
+
applicationData?: Hex.Hex
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DefaultSessionManagerAddresses = [Constants.DefaultSessionManager]
|
|
24
|
+
|
|
25
|
+
export class Sessions {
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly shared: Shared,
|
|
28
|
+
private readonly sessionManagerAddresses: Address.Address[] = DefaultSessionManagerAddresses,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async getSessionTopology(walletAddress: Address.Address): Promise<SessionConfig.SessionsTopology> {
|
|
32
|
+
const { modules } = await this.shared.modules.wallets.getConfigurationParts(walletAddress)
|
|
33
|
+
const managerLeaf = modules.find((leaf) =>
|
|
34
|
+
this.sessionManagerAddresses.some((addr) => Address.isEqual(addr, leaf.address)),
|
|
35
|
+
)
|
|
36
|
+
if (!managerLeaf) {
|
|
37
|
+
throw new Error('Session manager not found')
|
|
38
|
+
}
|
|
39
|
+
const imageHash = managerLeaf.imageHash
|
|
40
|
+
const tree = await this.shared.sequence.stateProvider.getTree(imageHash)
|
|
41
|
+
if (!tree) {
|
|
42
|
+
throw new Error('Session topology not found')
|
|
43
|
+
}
|
|
44
|
+
return SessionConfig.configurationTreeToSessionsTopology(tree)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async prepareAuthorizeImplicitSession(
|
|
48
|
+
walletAddress: Address.Address,
|
|
49
|
+
sessionAddress: Address.Address,
|
|
50
|
+
args: AuthorizeImplicitSessionArgs,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
const topology = await this.getSessionTopology(walletAddress)
|
|
53
|
+
const identitySignerAddress = SessionConfig.getIdentitySigner(topology)
|
|
54
|
+
if (!identitySignerAddress) {
|
|
55
|
+
throw new Error('No identity signer address found')
|
|
56
|
+
}
|
|
57
|
+
const identityKind = await this.shared.modules.signers.kindOf(walletAddress, identitySignerAddress)
|
|
58
|
+
if (!identityKind) {
|
|
59
|
+
throw new Error('No identity handler kind found')
|
|
60
|
+
}
|
|
61
|
+
const handler = this.shared.handlers.get(identityKind)
|
|
62
|
+
if (!handler) {
|
|
63
|
+
throw new Error('No identity handler found')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create the attestation to sign
|
|
67
|
+
let identityType: IdentityType | undefined
|
|
68
|
+
let issuerHash: Hex.Hex = '0x'
|
|
69
|
+
let audienceHash: Hex.Hex = '0x'
|
|
70
|
+
if (handler instanceof IdentityHandler) {
|
|
71
|
+
identityType = handler.identityType
|
|
72
|
+
if (handler instanceof AuthCodePkceHandler) {
|
|
73
|
+
issuerHash = Hash.keccak256(Hex.fromString(handler.issuer))
|
|
74
|
+
audienceHash = Hash.keccak256(Hex.fromString(handler.audience))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const attestation: Attestation.Attestation = {
|
|
78
|
+
approvedSigner: sessionAddress,
|
|
79
|
+
identityType: Bytes.fromHex(identityTypeToHex(identityType), { size: 4 }),
|
|
80
|
+
issuerHash: Bytes.fromHex(issuerHash, { size: 32 }),
|
|
81
|
+
audienceHash: Bytes.fromHex(audienceHash, { size: 32 }),
|
|
82
|
+
applicationData: Bytes.fromHex(args.applicationData ?? '0x'),
|
|
83
|
+
authData: {
|
|
84
|
+
redirectUrl: args.target,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
// Fake the configuration with the single required signer
|
|
88
|
+
const configuration: Config.Config = {
|
|
89
|
+
threshold: 1n,
|
|
90
|
+
checkpoint: 0n,
|
|
91
|
+
topology: {
|
|
92
|
+
type: 'signer',
|
|
93
|
+
address: identitySignerAddress,
|
|
94
|
+
weight: 1n,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
const envelope: Envelope.Envelope<Payload.SessionImplicitAuthorize> = {
|
|
98
|
+
payload: {
|
|
99
|
+
type: 'session-implicit-authorize',
|
|
100
|
+
sessionAddress,
|
|
101
|
+
attestation,
|
|
102
|
+
},
|
|
103
|
+
wallet: walletAddress,
|
|
104
|
+
chainId: 0n,
|
|
105
|
+
configuration,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Request the signature from the identity handler
|
|
109
|
+
return this.shared.modules.signatures.request(envelope, 'session-implicit-authorize', {
|
|
110
|
+
origin: args.target,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async completeAuthorizeImplicitSession(requestId: string): Promise<{
|
|
115
|
+
attestation: Attestation.Attestation
|
|
116
|
+
signature: SequenceSignature.RSY
|
|
117
|
+
}> {
|
|
118
|
+
// Get the updated signature request
|
|
119
|
+
const signatureRequest = await this.shared.modules.signatures.get(requestId)
|
|
120
|
+
if (
|
|
121
|
+
signatureRequest.action !== 'session-implicit-authorize' ||
|
|
122
|
+
!Payload.isSessionImplicitAuthorize(signatureRequest.envelope.payload)
|
|
123
|
+
) {
|
|
124
|
+
throw new Error('Invalid action')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!Envelope.isSigned(signatureRequest.envelope) || !Envelope.reachedThreshold(signatureRequest.envelope)) {
|
|
128
|
+
throw new Error('Envelope not signed or threshold not reached')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Find any valid signature
|
|
132
|
+
const signature = signatureRequest.envelope.signatures[0]
|
|
133
|
+
if (!signature || !Envelope.isSignature(signature)) {
|
|
134
|
+
throw new Error('No valid signature found')
|
|
135
|
+
}
|
|
136
|
+
if (signature.signature.type !== 'hash') {
|
|
137
|
+
// Should never happen
|
|
138
|
+
throw new Error('Unsupported signature type')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
attestation: signatureRequest.envelope.payload.attestation,
|
|
143
|
+
signature: signature.signature,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async addExplicitSession(
|
|
148
|
+
walletAddress: Address.Address,
|
|
149
|
+
sessionAddress: Address.Address,
|
|
150
|
+
permissions: CoreSigners.Session.ExplicitParams,
|
|
151
|
+
origin?: string,
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
const topology = await this.getSessionTopology(walletAddress)
|
|
154
|
+
const newTopology = SessionConfig.addExplicitSession(topology, {
|
|
155
|
+
...permissions,
|
|
156
|
+
signer: sessionAddress,
|
|
157
|
+
})
|
|
158
|
+
return this.prepareSessionUpdate(walletAddress, newTopology, origin)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async removeExplicitSession(
|
|
162
|
+
walletAddress: Address.Address,
|
|
163
|
+
sessionAddress: Address.Address,
|
|
164
|
+
origin?: string,
|
|
165
|
+
): Promise<string> {
|
|
166
|
+
const topology = await this.getSessionTopology(walletAddress)
|
|
167
|
+
const newTopology = SessionConfig.removeExplicitSession(topology, sessionAddress)
|
|
168
|
+
if (!newTopology) {
|
|
169
|
+
throw new Error('Session not found')
|
|
170
|
+
}
|
|
171
|
+
return this.prepareSessionUpdate(walletAddress, newTopology, origin)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async addBlacklistAddress(
|
|
175
|
+
walletAddress: Address.Address,
|
|
176
|
+
address: Address.Address,
|
|
177
|
+
origin?: string,
|
|
178
|
+
): Promise<string> {
|
|
179
|
+
const topology = await this.getSessionTopology(walletAddress)
|
|
180
|
+
const newTopology = SessionConfig.addToImplicitBlacklist(topology, address)
|
|
181
|
+
return this.prepareSessionUpdate(walletAddress, newTopology, origin)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async removeBlacklistAddress(
|
|
185
|
+
walletAddress: Address.Address,
|
|
186
|
+
address: Address.Address,
|
|
187
|
+
origin?: string,
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
const topology = await this.getSessionTopology(walletAddress)
|
|
190
|
+
const newTopology = SessionConfig.removeFromImplicitBlacklist(topology, address)
|
|
191
|
+
return this.prepareSessionUpdate(walletAddress, newTopology, origin)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async prepareSessionUpdate(
|
|
195
|
+
walletAddress: Address.Address,
|
|
196
|
+
topology: SessionConfig.SessionsTopology,
|
|
197
|
+
origin: string = 'wallet-webapp',
|
|
198
|
+
): Promise<string> {
|
|
199
|
+
// Store the new configuration
|
|
200
|
+
const tree = SessionConfig.sessionsTopologyToConfigurationTree(topology)
|
|
201
|
+
await this.shared.sequence.stateProvider.saveTree(tree)
|
|
202
|
+
const newImageHash = GenericTree.hash(tree)
|
|
203
|
+
|
|
204
|
+
// Find the session manager in the old configuration
|
|
205
|
+
const { modules } = await this.shared.modules.wallets.getConfigurationParts(walletAddress)
|
|
206
|
+
const managerLeaf = modules.find((leaf) =>
|
|
207
|
+
this.sessionManagerAddresses.some((addr) => Address.isEqual(addr, leaf.address)),
|
|
208
|
+
)
|
|
209
|
+
if (!managerLeaf) {
|
|
210
|
+
// Missing. Add it
|
|
211
|
+
modules.push({
|
|
212
|
+
...ManagerOptionsDefaults.defaultSessionsTopology,
|
|
213
|
+
imageHash: newImageHash,
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
// Update the configuration to use the new session manager image hash
|
|
217
|
+
managerLeaf.imageHash = newImageHash
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return this.shared.modules.wallets.requestConfigurationUpdate(
|
|
221
|
+
walletAddress,
|
|
222
|
+
{
|
|
223
|
+
modules,
|
|
224
|
+
},
|
|
225
|
+
Actions.SessionUpdate,
|
|
226
|
+
origin,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async completeSessionUpdate(requestId: string) {
|
|
231
|
+
const sigRequest = await this.shared.modules.signatures.get(requestId)
|
|
232
|
+
if (sigRequest.action !== 'session-update' || !Payload.isConfigUpdate(sigRequest.envelope.payload)) {
|
|
233
|
+
throw new Error('Invalid action')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return this.shared.modules.wallets.completeConfigurationUpdate(requestId)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { Envelope } from '@0xsequence/wallet-core'
|
|
2
|
+
import { Config, Payload } from '@0xsequence/wallet-primitives'
|
|
3
|
+
import { Address, Hex } from 'ox'
|
|
4
|
+
import { v7 as uuidv7 } from 'uuid'
|
|
5
|
+
import { Shared } from './manager.js'
|
|
6
|
+
import {
|
|
7
|
+
Action,
|
|
8
|
+
ActionToPayload,
|
|
9
|
+
BaseSignatureRequest,
|
|
10
|
+
SignatureRequest,
|
|
11
|
+
SignerBase,
|
|
12
|
+
SignerSigned,
|
|
13
|
+
SignerUnavailable,
|
|
14
|
+
} from './types/signature-request.js'
|
|
15
|
+
import { Cron } from './cron.js'
|
|
16
|
+
|
|
17
|
+
export class Signatures {
|
|
18
|
+
constructor(private readonly shared: Shared) {}
|
|
19
|
+
|
|
20
|
+
initialize() {
|
|
21
|
+
this.shared.modules.cron.registerJob('prune-signatures', 10 * 60 * 1000, async () => {
|
|
22
|
+
const prunedSignatures = await this.prune()
|
|
23
|
+
if (prunedSignatures > 0) {
|
|
24
|
+
this.shared.modules.logger.log(`Pruned ${prunedSignatures} signatures`)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
this.shared.modules.logger.log('Signatures module initialized and job registered.')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async getBase(requestId: string): Promise<BaseSignatureRequest> {
|
|
31
|
+
const request = await this.shared.databases.signatures.get(requestId)
|
|
32
|
+
if (!request) {
|
|
33
|
+
throw new Error(`Request not found for ${requestId}`)
|
|
34
|
+
}
|
|
35
|
+
return request
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async list(): Promise<SignatureRequest[]> {
|
|
39
|
+
return this.shared.databases.signatures.list() as any as SignatureRequest[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async get(requestId: string): Promise<SignatureRequest> {
|
|
43
|
+
const request = await this.getBase(requestId)
|
|
44
|
+
|
|
45
|
+
if (request.status !== 'pending' && request.scheduledPruning < Date.now()) {
|
|
46
|
+
await this.shared.databases.signatures.del(requestId)
|
|
47
|
+
throw new Error(`Request not found for ${requestId}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const signers = Config.getSigners(request.envelope.configuration.topology)
|
|
51
|
+
const signersAndKinds = await Promise.all([
|
|
52
|
+
...signers.signers.map(async (signer) => {
|
|
53
|
+
const kind = await this.shared.modules.signers.kindOf(request.wallet, signer)
|
|
54
|
+
return {
|
|
55
|
+
address: signer,
|
|
56
|
+
imageHash: undefined,
|
|
57
|
+
kind,
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
...signers.sapientSigners.map(async (signer) => {
|
|
61
|
+
const kind = await this.shared.modules.signers.kindOf(
|
|
62
|
+
request.wallet,
|
|
63
|
+
signer.address,
|
|
64
|
+
Hex.from(signer.imageHash),
|
|
65
|
+
)
|
|
66
|
+
return {
|
|
67
|
+
address: signer.address,
|
|
68
|
+
imageHash: signer.imageHash,
|
|
69
|
+
kind,
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
const statuses = await Promise.all(
|
|
75
|
+
signersAndKinds.map(async (sak) => {
|
|
76
|
+
const base: SignerBase = {
|
|
77
|
+
address: sak.address,
|
|
78
|
+
imageHash: sak.imageHash,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// We may have a signature for this signer already
|
|
82
|
+
const signed = request.envelope.signatures.some((sig) => {
|
|
83
|
+
if (Envelope.isSapientSignature(sig)) {
|
|
84
|
+
return Address.isEqual(sig.signature.address, sak.address) && sig.imageHash === sak.imageHash
|
|
85
|
+
}
|
|
86
|
+
return Address.isEqual(sig.address, sak.address)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (!sak.kind) {
|
|
90
|
+
const status: SignerUnavailable = {
|
|
91
|
+
...base,
|
|
92
|
+
handler: undefined,
|
|
93
|
+
reason: 'unknown-signer-kind',
|
|
94
|
+
status: 'unavailable',
|
|
95
|
+
}
|
|
96
|
+
return status
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handler = this.shared.handlers.get(sak.kind)
|
|
100
|
+
if (signed) {
|
|
101
|
+
const status: SignerSigned = {
|
|
102
|
+
...base,
|
|
103
|
+
handler,
|
|
104
|
+
status: 'signed',
|
|
105
|
+
}
|
|
106
|
+
return status
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!handler) {
|
|
110
|
+
const status: SignerUnavailable = {
|
|
111
|
+
...base,
|
|
112
|
+
handler: undefined,
|
|
113
|
+
reason: 'no-handler',
|
|
114
|
+
status: 'unavailable',
|
|
115
|
+
}
|
|
116
|
+
return status
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return handler.status(sak.address, sak.imageHash, request)
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const signatureRequest: SignatureRequest = {
|
|
124
|
+
...request,
|
|
125
|
+
...Envelope.weightOf(request.envelope),
|
|
126
|
+
signers: statuses,
|
|
127
|
+
}
|
|
128
|
+
return signatureRequest
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onSignatureRequestUpdate(
|
|
132
|
+
requestId: string,
|
|
133
|
+
cb: (requests: SignatureRequest) => void,
|
|
134
|
+
onError?: (error: Error) => void,
|
|
135
|
+
trigger?: boolean,
|
|
136
|
+
) {
|
|
137
|
+
const undoDbListener = this.shared.databases.signatures.addListener(() => {
|
|
138
|
+
this.get(requestId)
|
|
139
|
+
.then((request) => cb(request))
|
|
140
|
+
.catch((error) => onError?.(error))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const undoHandlerListeners = Array.from(this.shared.handlers.values()).map((handler) =>
|
|
144
|
+
handler.onStatusChange(() => {
|
|
145
|
+
this.get(requestId)
|
|
146
|
+
.then((request) => cb(request))
|
|
147
|
+
.catch((error) => onError?.(error))
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (trigger) {
|
|
152
|
+
this.get(requestId)
|
|
153
|
+
.then((request) => cb(request))
|
|
154
|
+
.catch((error) => onError?.(error))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return () => {
|
|
158
|
+
undoDbListener()
|
|
159
|
+
undoHandlerListeners.forEach((undoFn) => undoFn())
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
onSignatureRequestsUpdate(cb: (requests: BaseSignatureRequest[]) => void, trigger?: boolean) {
|
|
164
|
+
const undo = this.shared.databases.signatures.addListener(() => {
|
|
165
|
+
this.list().then((l) => cb(l))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if (trigger) {
|
|
169
|
+
this.list().then((l) => cb(l))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return undo
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async complete(requestId: string) {
|
|
176
|
+
const request = await this.getBase(requestId)
|
|
177
|
+
|
|
178
|
+
if (request?.envelope.payload.type === 'config-update') {
|
|
179
|
+
// Clear pending config updates for the same wallet with a checkpoint equal or lower than the completed update
|
|
180
|
+
const pendingRequests = await this.shared.databases.signatures.list()
|
|
181
|
+
const pendingConfigUpdatesToClear = pendingRequests.filter(
|
|
182
|
+
(sig) =>
|
|
183
|
+
Address.isEqual(sig.wallet, request.wallet) &&
|
|
184
|
+
sig.envelope.payload.type === 'config-update' &&
|
|
185
|
+
sig.envelope.configuration.checkpoint <= request.envelope.configuration.checkpoint,
|
|
186
|
+
)
|
|
187
|
+
// This also deletes the requested id
|
|
188
|
+
await Promise.all(pendingConfigUpdatesToClear.map((sig) => this.shared.modules.signatures.cancel(sig.id)))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await this.shared.databases.signatures.set({
|
|
192
|
+
...request,
|
|
193
|
+
status: 'completed',
|
|
194
|
+
scheduledPruning: Date.now() + this.shared.databases.pruningInterval,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async request<A extends Action>(
|
|
199
|
+
envelope: Envelope.Envelope<ActionToPayload[A]>,
|
|
200
|
+
action: A,
|
|
201
|
+
options: {
|
|
202
|
+
origin?: string
|
|
203
|
+
} = {},
|
|
204
|
+
): Promise<string> {
|
|
205
|
+
// If the action is a config update, we need to remove all signature requests
|
|
206
|
+
// for the same wallet that also involve configuration updates
|
|
207
|
+
// as it may cause race conditions
|
|
208
|
+
// TODO: Eventually we should define a "delta configuration" signature request
|
|
209
|
+
if (Payload.isConfigUpdate(envelope.payload)) {
|
|
210
|
+
const pendingRequests = await this.shared.databases.signatures.list()
|
|
211
|
+
const pendingConfigUpdatesToClear = pendingRequests.filter(
|
|
212
|
+
(sig) => sig.wallet === envelope.wallet && Payload.isConfigUpdate(sig.envelope.payload),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
console.warn(
|
|
216
|
+
'Deleting conflicting configuration updates for wallet',
|
|
217
|
+
envelope.wallet,
|
|
218
|
+
pendingConfigUpdatesToClear.map((pc) => pc.id),
|
|
219
|
+
)
|
|
220
|
+
await Promise.all(pendingConfigUpdatesToClear.map((sig) => this.shared.modules.signatures.cancel(sig.id)))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const id = uuidv7()
|
|
224
|
+
|
|
225
|
+
await this.shared.databases.signatures.set({
|
|
226
|
+
id,
|
|
227
|
+
wallet: envelope.wallet,
|
|
228
|
+
envelope: Envelope.toSigned(envelope),
|
|
229
|
+
origin: options.origin ?? 'unknown',
|
|
230
|
+
action,
|
|
231
|
+
createdAt: new Date().toISOString(),
|
|
232
|
+
status: 'pending',
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return id
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async addSignature(requestId: string, signature: Envelope.SapientSignature | Envelope.Signature) {
|
|
239
|
+
const request = await this.getBase(requestId)
|
|
240
|
+
|
|
241
|
+
Envelope.addSignature(request.envelope, signature)
|
|
242
|
+
|
|
243
|
+
await this.shared.databases.signatures.set(request)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async cancel(requestId: string) {
|
|
247
|
+
const request = await this.getBase(requestId)
|
|
248
|
+
|
|
249
|
+
await this.shared.databases.signatures.set({
|
|
250
|
+
...request,
|
|
251
|
+
status: 'cancelled',
|
|
252
|
+
scheduledPruning: Date.now() + this.shared.databases.pruningInterval,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async prune() {
|
|
257
|
+
const now = Date.now()
|
|
258
|
+
const requests = await this.shared.databases.signatures.list()
|
|
259
|
+
const toPrune = requests.filter((req) => req.status !== 'pending' && req.scheduledPruning < now)
|
|
260
|
+
await Promise.all(toPrune.map((req) => this.shared.databases.signatures.del(req.id)))
|
|
261
|
+
return toPrune.length
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Payload } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { Address, Hex } from 'ox'
|
|
3
|
+
import { Shared } from './manager.js'
|
|
4
|
+
import { Kind, Kinds, SignerWithKind, WitnessExtraSignerKind } from './types/signer.js'
|
|
5
|
+
|
|
6
|
+
export function isWitnessExtraSignerKind(extra: any): extra is WitnessExtraSignerKind {
|
|
7
|
+
return typeof extra === 'object' && extra !== null && 'signerKind' in extra
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toKnownKind(kind: string): Kind {
|
|
11
|
+
if (Object.values(Kinds).includes(kind as Kind)) {
|
|
12
|
+
return kind as Kind
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.warn(`Unknown signer kind: ${kind}`)
|
|
16
|
+
|
|
17
|
+
return Kinds.Unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Signers is in charge to know (or figure out) the "kind" of each signer
|
|
21
|
+
// i.e., when a signature is requested, we only get address and imageHash (if sapient)
|
|
22
|
+
// this module takes care of figuring out the kind of signer (e.g., device, passkey, recovery, etc.)
|
|
23
|
+
export class Signers {
|
|
24
|
+
constructor(private readonly shared: Shared) {}
|
|
25
|
+
|
|
26
|
+
async kindOf(wallet: Address.Address, address: Address.Address, imageHash?: Hex.Hex): Promise<Kind | undefined> {
|
|
27
|
+
// // The device may be among the local devices, in that case it is a local device
|
|
28
|
+
// // TODO: Maybe signers shouldn't be getting in the way of devices, it feels like a
|
|
29
|
+
// // different concern
|
|
30
|
+
// if (await this.devices.has(address)) {
|
|
31
|
+
// return Kinds.LocalDevice
|
|
32
|
+
// }
|
|
33
|
+
|
|
34
|
+
// Some signers are known by the configuration of the wallet development kit, specifically
|
|
35
|
+
// some of the sapient signers, who always share the same address
|
|
36
|
+
if (Address.isEqual(this.shared.sequence.extensions.recovery, address)) {
|
|
37
|
+
return Kinds.Recovery
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// We need to use the state provider (and witness) this will tell us the kind of signer
|
|
41
|
+
// NOTICE: This looks expensive, but this operation should be cached by the state provider
|
|
42
|
+
const witness = await (imageHash
|
|
43
|
+
? this.shared.sequence.stateProvider.getWitnessForSapient(wallet, address, imageHash)
|
|
44
|
+
: this.shared.sequence.stateProvider.getWitnessFor(wallet, address))
|
|
45
|
+
|
|
46
|
+
if (!witness) {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parse the payload, it may have the kind of signer
|
|
51
|
+
if (!Payload.isMessage(witness.payload)) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const message = JSON.parse(Hex.toString(witness.payload.message))
|
|
57
|
+
if (isWitnessExtraSignerKind(message)) {
|
|
58
|
+
return toKnownKind(message.signerKind)
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async resolveKinds(
|
|
66
|
+
wallet: Address.Address,
|
|
67
|
+
signers: (Address.Address | { address: Address.Address; imageHash: Hex.Hex })[],
|
|
68
|
+
): Promise<SignerWithKind[]> {
|
|
69
|
+
return Promise.all(
|
|
70
|
+
signers.map(async (signer) => {
|
|
71
|
+
if (typeof signer === 'string') {
|
|
72
|
+
const kind = await this.kindOf(wallet, signer)
|
|
73
|
+
return {
|
|
74
|
+
address: signer,
|
|
75
|
+
kind,
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
const kind = await this.kindOf(wallet, signer.address, signer.imageHash)
|
|
79
|
+
return {
|
|
80
|
+
address: signer.address,
|
|
81
|
+
imageHash: signer.imageHash,
|
|
82
|
+
kind,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|