@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.
Files changed (173) hide show
  1. package/.env.test +3 -0
  2. package/.turbo/turbo-build.log +5 -0
  3. package/CHANGELOG.md +11 -0
  4. package/LICENSE +202 -0
  5. package/dist/dbs/auth-commitments.d.ts +17 -0
  6. package/dist/dbs/auth-commitments.d.ts.map +1 -0
  7. package/dist/dbs/auth-commitments.js +13 -0
  8. package/dist/dbs/auth-keys.d.ts +19 -0
  9. package/dist/dbs/auth-keys.d.ts.map +1 -0
  10. package/dist/dbs/auth-keys.js +67 -0
  11. package/dist/dbs/generic.d.ts +33 -0
  12. package/dist/dbs/generic.d.ts.map +1 -0
  13. package/dist/dbs/generic.js +170 -0
  14. package/dist/dbs/index.d.ts +12 -0
  15. package/dist/dbs/index.d.ts.map +1 -0
  16. package/dist/dbs/index.js +8 -0
  17. package/dist/dbs/messages.d.ts +6 -0
  18. package/dist/dbs/messages.d.ts.map +1 -0
  19. package/dist/dbs/messages.js +13 -0
  20. package/dist/dbs/recovery.d.ts +6 -0
  21. package/dist/dbs/recovery.d.ts.map +1 -0
  22. package/dist/dbs/recovery.js +13 -0
  23. package/dist/dbs/signatures.d.ts +6 -0
  24. package/dist/dbs/signatures.d.ts.map +1 -0
  25. package/dist/dbs/signatures.js +13 -0
  26. package/dist/dbs/transactions.d.ts +6 -0
  27. package/dist/dbs/transactions.d.ts.map +1 -0
  28. package/dist/dbs/transactions.js +13 -0
  29. package/dist/dbs/wallets.d.ts +6 -0
  30. package/dist/dbs/wallets.d.ts.map +1 -0
  31. package/dist/dbs/wallets.js +13 -0
  32. package/dist/identity/signer.d.ts +17 -0
  33. package/dist/identity/signer.d.ts.map +1 -0
  34. package/dist/identity/signer.js +58 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +2 -0
  38. package/dist/sequence/cron.d.ts +19 -0
  39. package/dist/sequence/cron.d.ts.map +1 -0
  40. package/dist/sequence/cron.js +118 -0
  41. package/dist/sequence/devices.d.ts +14 -0
  42. package/dist/sequence/devices.d.ts.map +1 -0
  43. package/dist/sequence/devices.js +43 -0
  44. package/dist/sequence/handlers/authcode-pkce.d.ts +14 -0
  45. package/dist/sequence/handlers/authcode-pkce.d.ts.map +1 -0
  46. package/dist/sequence/handlers/authcode-pkce.js +48 -0
  47. package/dist/sequence/handlers/authcode.d.ts +25 -0
  48. package/dist/sequence/handlers/authcode.d.ts.map +1 -0
  49. package/dist/sequence/handlers/authcode.js +91 -0
  50. package/dist/sequence/handlers/devices.d.ts +14 -0
  51. package/dist/sequence/handlers/devices.d.ts.map +1 -0
  52. package/dist/sequence/handlers/devices.js +39 -0
  53. package/dist/sequence/handlers/handler.d.ts +8 -0
  54. package/dist/sequence/handlers/handler.d.ts.map +1 -0
  55. package/dist/sequence/handlers/handler.js +1 -0
  56. package/dist/sequence/handlers/identity.d.ts +21 -0
  57. package/dist/sequence/handlers/identity.d.ts.map +1 -0
  58. package/dist/sequence/handlers/identity.js +86 -0
  59. package/dist/sequence/handlers/index.d.ts +7 -0
  60. package/dist/sequence/handlers/index.d.ts.map +1 -0
  61. package/dist/sequence/handlers/index.js +5 -0
  62. package/dist/sequence/handlers/mnemonic.d.ts +19 -0
  63. package/dist/sequence/handlers/mnemonic.d.ts.map +1 -0
  64. package/dist/sequence/handlers/mnemonic.js +67 -0
  65. package/dist/sequence/handlers/otp.d.ts +20 -0
  66. package/dist/sequence/handlers/otp.d.ts.map +1 -0
  67. package/dist/sequence/handlers/otp.js +83 -0
  68. package/dist/sequence/handlers/passkeys.d.ts +17 -0
  69. package/dist/sequence/handlers/passkeys.d.ts.map +1 -0
  70. package/dist/sequence/handlers/passkeys.js +63 -0
  71. package/dist/sequence/handlers/recovery.d.ts +15 -0
  72. package/dist/sequence/handlers/recovery.d.ts.map +1 -0
  73. package/dist/sequence/handlers/recovery.js +72 -0
  74. package/dist/sequence/index.d.ts +12 -0
  75. package/dist/sequence/index.d.ts.map +1 -0
  76. package/dist/sequence/index.js +9 -0
  77. package/dist/sequence/logger.d.ts +7 -0
  78. package/dist/sequence/logger.d.ts.map +1 -0
  79. package/dist/sequence/logger.js +11 -0
  80. package/dist/sequence/manager.d.ts +287 -0
  81. package/dist/sequence/manager.d.ts.map +1 -0
  82. package/dist/sequence/manager.js +356 -0
  83. package/dist/sequence/messages.d.ts +18 -0
  84. package/dist/sequence/messages.d.ts.map +1 -0
  85. package/dist/sequence/messages.js +115 -0
  86. package/dist/sequence/recovery.d.ts +30 -0
  87. package/dist/sequence/recovery.d.ts.map +1 -0
  88. package/dist/sequence/recovery.js +314 -0
  89. package/dist/sequence/sessions.d.ts +26 -0
  90. package/dist/sequence/sessions.d.ts.map +1 -0
  91. package/dist/sequence/sessions.js +169 -0
  92. package/dist/sequence/signatures.d.ts +21 -0
  93. package/dist/sequence/signatures.d.ts.map +1 -0
  94. package/dist/sequence/signatures.js +192 -0
  95. package/dist/sequence/signers.d.ts +14 -0
  96. package/dist/sequence/signers.d.ts.map +1 -0
  97. package/dist/sequence/signers.js +74 -0
  98. package/dist/sequence/transactions.d.ts +26 -0
  99. package/dist/sequence/transactions.d.ts.map +1 -0
  100. package/dist/sequence/transactions.js +201 -0
  101. package/dist/sequence/types/index.d.ts +9 -0
  102. package/dist/sequence/types/index.d.ts.map +1 -0
  103. package/dist/sequence/types/index.js +2 -0
  104. package/dist/sequence/types/message-request.d.ts +23 -0
  105. package/dist/sequence/types/message-request.d.ts.map +1 -0
  106. package/dist/sequence/types/message-request.js +1 -0
  107. package/dist/sequence/types/recovery.d.ts +15 -0
  108. package/dist/sequence/types/recovery.d.ts.map +1 -0
  109. package/dist/sequence/types/recovery.js +1 -0
  110. package/dist/sequence/types/signature-request.d.ts +76 -0
  111. package/dist/sequence/types/signature-request.d.ts.map +1 -0
  112. package/dist/sequence/types/signature-request.js +11 -0
  113. package/dist/sequence/types/signer.d.ts +28 -0
  114. package/dist/sequence/types/signer.d.ts.map +1 -0
  115. package/dist/sequence/types/signer.js +10 -0
  116. package/dist/sequence/types/transaction-request.d.ts +41 -0
  117. package/dist/sequence/types/transaction-request.d.ts.map +1 -0
  118. package/dist/sequence/types/transaction-request.js +1 -0
  119. package/dist/sequence/types/wallet.d.ts +21 -0
  120. package/dist/sequence/types/wallet.d.ts.map +1 -0
  121. package/dist/sequence/types/wallet.js +1 -0
  122. package/dist/sequence/wallets.d.ts +121 -0
  123. package/dist/sequence/wallets.d.ts.map +1 -0
  124. package/dist/sequence/wallets.js +632 -0
  125. package/package.json +40 -0
  126. package/src/dbs/auth-commitments.ts +26 -0
  127. package/src/dbs/auth-keys.ts +85 -0
  128. package/src/dbs/generic.ts +194 -0
  129. package/src/dbs/index.ts +13 -0
  130. package/src/dbs/messages.ts +16 -0
  131. package/src/dbs/recovery.ts +15 -0
  132. package/src/dbs/signatures.ts +15 -0
  133. package/src/dbs/transactions.ts +16 -0
  134. package/src/dbs/wallets.ts +16 -0
  135. package/src/identity/signer.ts +78 -0
  136. package/src/index.ts +2 -0
  137. package/src/sequence/cron.ts +134 -0
  138. package/src/sequence/devices.ts +53 -0
  139. package/src/sequence/handlers/authcode-pkce.ts +70 -0
  140. package/src/sequence/handlers/authcode.ts +116 -0
  141. package/src/sequence/handlers/devices.ts +53 -0
  142. package/src/sequence/handlers/handler.ts +14 -0
  143. package/src/sequence/handlers/identity.ts +101 -0
  144. package/src/sequence/handlers/index.ts +6 -0
  145. package/src/sequence/handlers/mnemonic.ts +88 -0
  146. package/src/sequence/handlers/otp.ts +107 -0
  147. package/src/sequence/handlers/passkeys.ts +84 -0
  148. package/src/sequence/handlers/recovery.ts +88 -0
  149. package/src/sequence/index.ts +25 -0
  150. package/src/sequence/logger.ts +11 -0
  151. package/src/sequence/manager.ts +634 -0
  152. package/src/sequence/messages.ts +146 -0
  153. package/src/sequence/recovery.ts +429 -0
  154. package/src/sequence/sessions.ts +238 -0
  155. package/src/sequence/signatures.ts +263 -0
  156. package/src/sequence/signers.ts +88 -0
  157. package/src/sequence/transactions.ts +281 -0
  158. package/src/sequence/types/index.ts +27 -0
  159. package/src/sequence/types/message-request.ts +26 -0
  160. package/src/sequence/types/recovery.ts +15 -0
  161. package/src/sequence/types/signature-request.ts +89 -0
  162. package/src/sequence/types/signer.ts +32 -0
  163. package/src/sequence/types/transaction-request.ts +47 -0
  164. package/src/sequence/types/wallet.ts +24 -0
  165. package/src/sequence/wallets.ts +853 -0
  166. package/test/constants.ts +62 -0
  167. package/test/recovery.test.ts +211 -0
  168. package/test/sessions.test.ts +324 -0
  169. package/test/setup.ts +63 -0
  170. package/test/transactions.test.ts +464 -0
  171. package/test/wallets.test.ts +381 -0
  172. package/tsconfig.json +10 -0
  173. 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
+ }