@bsv/wallet-toolbox 1.1.61 → 1.2.1

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 (199) hide show
  1. package/docs/client.md +2339 -182
  2. package/docs/wallet.md +2339 -182
  3. package/out/src/CWIStyleWalletManager.d.ts +417 -0
  4. package/out/src/CWIStyleWalletManager.d.ts.map +1 -0
  5. package/out/src/CWIStyleWalletManager.js +1153 -0
  6. package/out/src/CWIStyleWalletManager.js.map +1 -0
  7. package/out/src/SimpleWalletManager.d.ts +169 -0
  8. package/out/src/SimpleWalletManager.d.ts.map +1 -0
  9. package/out/src/SimpleWalletManager.js +315 -0
  10. package/out/src/SimpleWalletManager.js.map +1 -0
  11. package/out/src/Wallet.d.ts +6 -1
  12. package/out/src/Wallet.d.ts.map +1 -1
  13. package/out/src/Wallet.js +39 -7
  14. package/out/src/Wallet.js.map +1 -1
  15. package/out/src/WalletAuthenticationManager.d.ts +33 -0
  16. package/out/src/WalletAuthenticationManager.d.ts.map +1 -0
  17. package/out/src/WalletAuthenticationManager.js +110 -0
  18. package/out/src/WalletAuthenticationManager.js.map +1 -0
  19. package/out/src/WalletPermissionsManager.d.ts +575 -0
  20. package/out/src/WalletPermissionsManager.d.ts.map +1 -0
  21. package/out/src/WalletPermissionsManager.js +1789 -0
  22. package/out/src/WalletPermissionsManager.js.map +1 -0
  23. package/out/src/WalletSettingsManager.d.ts +59 -0
  24. package/out/src/WalletSettingsManager.d.ts.map +1 -0
  25. package/out/src/WalletSettingsManager.js +189 -0
  26. package/out/src/WalletSettingsManager.js.map +1 -0
  27. package/out/src/__tests/CWIStyleWalletManager.test.d.ts +2 -0
  28. package/out/src/__tests/CWIStyleWalletManager.test.d.ts.map +1 -0
  29. package/out/src/__tests/CWIStyleWalletManager.test.js +471 -0
  30. package/out/src/__tests/CWIStyleWalletManager.test.js.map +1 -0
  31. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts +2 -0
  32. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts.map +1 -0
  33. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +239 -0
  34. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +1 -0
  35. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts +2 -0
  36. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts.map +1 -0
  37. package/out/src/__tests/WalletPermissionsManager.checks.test.js +637 -0
  38. package/out/src/__tests/WalletPermissionsManager.checks.test.js.map +1 -0
  39. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts +2 -0
  40. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts.map +1 -0
  41. package/out/src/__tests/WalletPermissionsManager.encryption.test.js +295 -0
  42. package/out/src/__tests/WalletPermissionsManager.encryption.test.js.map +1 -0
  43. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts +83 -0
  44. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +1 -0
  45. package/out/src/__tests/WalletPermissionsManager.fixtures.js +261 -0
  46. package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +1 -0
  47. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts +2 -0
  48. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts.map +1 -0
  49. package/out/src/__tests/WalletPermissionsManager.flows.test.js +377 -0
  50. package/out/src/__tests/WalletPermissionsManager.flows.test.js.map +1 -0
  51. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts +2 -0
  52. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts.map +1 -0
  53. package/out/src/__tests/WalletPermissionsManager.initialization.test.js +227 -0
  54. package/out/src/__tests/WalletPermissionsManager.initialization.test.js.map +1 -0
  55. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts +2 -0
  56. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts.map +1 -0
  57. package/out/src/__tests/WalletPermissionsManager.proxying.test.js +566 -0
  58. package/out/src/__tests/WalletPermissionsManager.proxying.test.js.map +1 -0
  59. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts +2 -0
  60. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts.map +1 -0
  61. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +454 -0
  62. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +1 -0
  63. package/out/src/index.all.d.ts +9 -0
  64. package/out/src/index.all.d.ts.map +1 -1
  65. package/out/src/index.all.js +9 -0
  66. package/out/src/index.all.js.map +1 -1
  67. package/out/src/index.client.d.ts +9 -0
  68. package/out/src/index.client.d.ts.map +1 -1
  69. package/out/src/index.client.js +9 -0
  70. package/out/src/index.client.js.map +1 -1
  71. package/out/src/sdk/CertOpsWallet.d.ts +7 -0
  72. package/out/src/sdk/CertOpsWallet.d.ts.map +1 -0
  73. package/out/src/sdk/CertOpsWallet.js +3 -0
  74. package/out/src/sdk/CertOpsWallet.js.map +1 -0
  75. package/out/src/sdk/__test/CertificateLifeCycle.test.js +19 -82
  76. package/out/src/sdk/__test/CertificateLifeCycle.test.js.map +1 -1
  77. package/out/src/sdk/index.d.ts +1 -1
  78. package/out/src/sdk/index.d.ts.map +1 -1
  79. package/out/src/sdk/index.js +1 -1
  80. package/out/src/sdk/index.js.map +1 -1
  81. package/out/src/sdk/validationHelpers.d.ts.map +1 -1
  82. package/out/src/sdk/validationHelpers.js +13 -12
  83. package/out/src/sdk/validationHelpers.js.map +1 -1
  84. package/out/src/services/__tests/ARC.test.js +9 -0
  85. package/out/src/services/__tests/ARC.test.js.map +1 -1
  86. package/out/src/services/__tests/bitrails.test.js +7 -2
  87. package/out/src/services/__tests/bitrails.test.js.map +1 -1
  88. package/out/src/services/providers/ARC.js +1 -1
  89. package/out/src/services/providers/ARC.js.map +1 -1
  90. package/out/src/services/providers/__tests/WhatsOnChain.test.js +3 -3
  91. package/out/src/services/providers/__tests/WhatsOnChain.test.js.map +1 -1
  92. package/out/src/signer/methods/proveCertificate.d.ts.map +1 -1
  93. package/out/src/signer/methods/proveCertificate.js +3 -19
  94. package/out/src/signer/methods/proveCertificate.js.map +1 -1
  95. package/out/src/storage/__test/WalletStorageManager.test.js +1 -1
  96. package/out/src/storage/__test/WalletStorageManager.test.js.map +1 -1
  97. package/out/src/storage/methods/listOutputs.js +1 -1
  98. package/out/src/storage/methods/listOutputs.js.map +1 -1
  99. package/out/src/storage/remoting/StorageClient.d.ts +2 -2
  100. package/out/src/storage/remoting/StorageClient.d.ts.map +1 -1
  101. package/out/src/storage/remoting/StorageClient.js +1 -1
  102. package/out/src/storage/remoting/StorageClient.js.map +1 -1
  103. package/out/src/utility/identityUtils.d.ts +31 -0
  104. package/out/src/utility/identityUtils.d.ts.map +1 -0
  105. package/out/src/utility/identityUtils.js +116 -0
  106. package/out/src/utility/identityUtils.js.map +1 -0
  107. package/out/src/wab-client/WABClient.d.ts +49 -0
  108. package/out/src/wab-client/WABClient.d.ts.map +1 -0
  109. package/out/src/wab-client/WABClient.js +83 -0
  110. package/out/src/wab-client/WABClient.js.map +1 -0
  111. package/out/src/wab-client/__tests/WABClient.man.test.d.ts +2 -0
  112. package/out/src/wab-client/__tests/WABClient.man.test.d.ts.map +1 -0
  113. package/out/src/wab-client/__tests/WABClient.man.test.js +52 -0
  114. package/out/src/wab-client/__tests/WABClient.man.test.js.map +1 -0
  115. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +34 -0
  116. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -0
  117. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -0
  118. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -0
  119. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts +7 -0
  120. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts.map +1 -0
  121. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js +36 -0
  122. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js.map +1 -0
  123. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +28 -0
  124. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -0
  125. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +69 -0
  126. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -0
  127. package/out/test/Wallet/action/internalizeAction.a.test.js +1 -1
  128. package/out/test/Wallet/action/internalizeAction.a.test.js.map +1 -1
  129. package/out/test/Wallet/certificate/acquireCertificate.test.js +26 -29
  130. package/out/test/Wallet/certificate/acquireCertificate.test.js.map +1 -1
  131. package/out/test/Wallet/local/localWallet.man.test.d.ts.map +1 -1
  132. package/out/test/Wallet/local/localWallet.man.test.js +25 -10
  133. package/out/test/Wallet/local/localWallet.man.test.js.map +1 -1
  134. package/out/test/storage/KnexMigrations.test.js +1 -1
  135. package/out/test/storage/KnexMigrations.test.js.map +1 -1
  136. package/out/test/storage/update.test.js +1 -1
  137. package/out/test/storage/update.test.js.map +1 -1
  138. package/out/test/utils/TestUtilsWalletStorage.d.ts +9 -5
  139. package/out/test/utils/TestUtilsWalletStorage.d.ts.map +1 -1
  140. package/out/test/utils/TestUtilsWalletStorage.js +15 -9
  141. package/out/test/utils/TestUtilsWalletStorage.js.map +1 -1
  142. package/out/test/wallet/action/internalizeAction.test.js +1 -1
  143. package/out/test/wallet/action/internalizeAction.test.js.map +1 -1
  144. package/out/test/wallet/list/listActions2.test.js +1 -1
  145. package/out/test/wallet/list/listActions2.test.js.map +1 -1
  146. package/out/test/wallet/sync/Wallet.sync.test.js +1 -1
  147. package/out/test/wallet/sync/Wallet.sync.test.js.map +1 -1
  148. package/out/tsconfig.all.tsbuildinfo +1 -1
  149. package/package.json +3 -4
  150. package/src/CWIStyleWalletManager.ts +1738 -0
  151. package/src/SimpleWalletManager.ts +526 -0
  152. package/src/Wallet.ts +70 -7
  153. package/src/WalletAuthenticationManager.ts +150 -0
  154. package/src/WalletPermissionsManager.ts +2424 -0
  155. package/src/WalletSettingsManager.ts +243 -0
  156. package/src/__tests/CWIStyleWalletManager.test.ts +604 -0
  157. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +323 -0
  158. package/src/__tests/WalletPermissionsManager.checks.test.ts +839 -0
  159. package/src/__tests/WalletPermissionsManager.encryption.test.ts +370 -0
  160. package/src/__tests/WalletPermissionsManager.fixtures.ts +284 -0
  161. package/src/__tests/WalletPermissionsManager.flows.test.ts +457 -0
  162. package/src/__tests/WalletPermissionsManager.initialization.test.ts +300 -0
  163. package/src/__tests/WalletPermissionsManager.proxying.test.ts +706 -0
  164. package/src/__tests/WalletPermissionsManager.tokens.test.ts +546 -0
  165. package/src/index.all.ts +9 -0
  166. package/src/index.client.ts +9 -0
  167. package/src/sdk/CertOpsWallet.ts +18 -0
  168. package/src/sdk/__test/CertificateLifeCycle.test.ts +66 -113
  169. package/src/sdk/index.ts +1 -1
  170. package/src/sdk/validationHelpers.ts +12 -11
  171. package/src/services/__tests/ARC.test.ts +14 -1
  172. package/src/services/__tests/bitrails.test.ts +7 -2
  173. package/src/services/processingErrors/arcSuccessError.json +76 -0
  174. package/src/services/providers/ARC.ts +1 -1
  175. package/src/services/providers/__tests/WhatsOnChain.test.ts +3 -3
  176. package/src/signer/methods/proveCertificate.ts +14 -21
  177. package/src/storage/__test/WalletStorageManager.test.ts +1 -1
  178. package/src/storage/methods/listOutputs.ts +1 -1
  179. package/src/storage/remoting/StorageClient.ts +4 -4
  180. package/src/utility/identityUtils.ts +159 -0
  181. package/src/wab-client/WABClient.ts +94 -0
  182. package/src/wab-client/__tests/WABClient.man.test.ts +59 -0
  183. package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +47 -0
  184. package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +35 -0
  185. package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +72 -0
  186. package/test/Wallet/action/internalizeAction.a.test.ts +1 -1
  187. package/test/Wallet/certificate/acquireCertificate.test.ts +89 -30
  188. package/test/Wallet/local/localWallet.man.test.ts +20 -4
  189. package/test/storage/KnexMigrations.test.ts +1 -1
  190. package/test/storage/update.test.ts +1 -1
  191. package/test/utils/TestUtilsWalletStorage.ts +24 -13
  192. package/test/wallet/action/internalizeAction.test.ts +1 -1
  193. package/test/wallet/list/listActions2.test.ts +1 -1
  194. package/test/wallet/sync/Wallet.sync.test.ts +1 -1
  195. package/out/src/sdk/CertOps.d.ts +0 -66
  196. package/out/src/sdk/CertOps.d.ts.map +0 -1
  197. package/out/src/sdk/CertOps.js +0 -190
  198. package/out/src/sdk/CertOps.js.map +0 -1
  199. package/src/sdk/CertOps.ts +0 -274
@@ -0,0 +1,1738 @@
1
+ import {
2
+ Hash,
3
+ Utils,
4
+ Random,
5
+ SymmetricKey,
6
+ AbortActionArgs,
7
+ AbortActionResult,
8
+ AcquireCertificateArgs,
9
+ AcquireCertificateResult,
10
+ AuthenticatedResult,
11
+ CreateActionArgs,
12
+ CreateActionResult,
13
+ CreateHmacArgs,
14
+ CreateHmacResult,
15
+ CreateSignatureArgs,
16
+ CreateSignatureResult,
17
+ DiscoverByAttributesArgs,
18
+ DiscoverByIdentityKeyArgs,
19
+ DiscoverCertificatesResult,
20
+ GetHeaderArgs,
21
+ GetHeaderResult,
22
+ GetHeightResult,
23
+ GetNetworkResult,
24
+ GetPublicKeyArgs,
25
+ GetPublicKeyResult,
26
+ GetVersionResult,
27
+ InternalizeActionArgs,
28
+ InternalizeActionResult,
29
+ ListActionsArgs,
30
+ ListActionsResult,
31
+ ListCertificatesArgs,
32
+ ListCertificatesResult,
33
+ ListOutputsArgs,
34
+ ListOutputsResult,
35
+ OriginatorDomainNameStringUnder250Bytes,
36
+ ProveCertificateArgs,
37
+ ProveCertificateResult,
38
+ RelinquishCertificateArgs,
39
+ RelinquishCertificateResult,
40
+ RelinquishOutputArgs,
41
+ RelinquishOutputResult,
42
+ RevealCounterpartyKeyLinkageArgs,
43
+ RevealCounterpartyKeyLinkageResult,
44
+ RevealSpecificKeyLinkageArgs,
45
+ RevealSpecificKeyLinkageResult,
46
+ SignActionArgs,
47
+ SignActionResult,
48
+ VerifyHmacArgs,
49
+ VerifyHmacResult,
50
+ VerifySignatureArgs,
51
+ VerifySignatureResult,
52
+ WalletDecryptArgs,
53
+ WalletDecryptResult,
54
+ WalletEncryptArgs,
55
+ WalletEncryptResult,
56
+ WalletInterface,
57
+ OutpointString,
58
+ PrivateKey,
59
+ LookupResolver,
60
+ LookupAnswer,
61
+ Transaction,
62
+ PushDrop,
63
+ CreateActionInput,
64
+ SHIPBroadcaster,
65
+ HTTPSOverlayBroadcastFacilitator
66
+ } from '@bsv/sdk'
67
+ import { PrivilegedKeyManager } from './sdk/PrivilegedKeyManager'
68
+
69
+ /**
70
+ * Number of rounds used in PBKDF2 for deriving password keys.
71
+ */
72
+ export const PBKDF2_NUM_ROUNDS = 7777
73
+
74
+ /**
75
+ * Describes the structure of a User Management Protocol (UMP) token.
76
+ */
77
+ export interface UMPToken {
78
+ /**
79
+ * Primary key encrypted by the XOR of the password and presentation keys.
80
+ */
81
+ passwordPresentationPrimary: number[]
82
+
83
+ /**
84
+ * Primary key encrypted by the XOR of the password and recovery keys.
85
+ */
86
+ passwordRecoveryPrimary: number[]
87
+
88
+ /**
89
+ * Primary key encrypted by the XOR of the presentation and recovery keys.
90
+ */
91
+ presentationRecoveryPrimary: number[]
92
+
93
+ /**
94
+ * Privileged key encrypted by the XOR of the password and primary keys.
95
+ */
96
+ passwordPrimaryPrivileged: number[]
97
+
98
+ /**
99
+ * Privileged key encrypted by the XOR of the presentation and recovery keys.
100
+ */
101
+ presentationRecoveryPrivileged: number[]
102
+
103
+ /**
104
+ * Hash of the presentation key.
105
+ */
106
+ presentationHash: number[]
107
+
108
+ /**
109
+ * PBKDF2 salt used in conjunction with the password to derive the password key.
110
+ */
111
+ passwordSalt: number[]
112
+
113
+ /**
114
+ * Hash of the recovery key.
115
+ */
116
+ recoveryHash: number[]
117
+
118
+ /**
119
+ * A copy of the presentation key encrypted with the privileged key.
120
+ */
121
+ presentationKeyEncrypted: number[]
122
+
123
+ /**
124
+ * A copy of the recovery key encrypted with the privileged key.
125
+ */
126
+ recoveryKeyEncrypted: number[]
127
+
128
+ /**
129
+ * A copy of the password key encrypted with the privileged key.
130
+ */
131
+ passwordKeyEncrypted: number[]
132
+
133
+ /**
134
+ * Describes the token's location on-chain, if it's already been published.
135
+ */
136
+ currentOutpoint?: OutpointString
137
+ }
138
+
139
+ /**
140
+ * Describes a system capable of finding and updating UMP tokens on the blockchain.
141
+ */
142
+ export interface UMPTokenInteractor {
143
+ /**
144
+ * Locates the latest valid copy of a UMP token (including its outpoint)
145
+ * based on the presentation key hash.
146
+ *
147
+ * @param hash The hash of the presentation key.
148
+ * @returns The UMP token if found; otherwise, undefined.
149
+ */
150
+ findByPresentationKeyHash: (hash: number[]) => Promise<UMPToken | undefined>
151
+
152
+ /**
153
+ * Locates the latest valid copy of a UMP token (including its outpoint)
154
+ * based on the recovery key hash.
155
+ *
156
+ * @param hash The hash of the recovery key.
157
+ * @returns The UMP token if found; otherwise, undefined.
158
+ */
159
+ findByRecoveryKeyHash: (hash: number[]) => Promise<UMPToken | undefined>
160
+
161
+ /**
162
+ * Creates (and optionally consumes the previous version of) a UMP token on-chain.
163
+ *
164
+ * @param wallet The wallet that might be used to create a new token.
165
+ * @param adminOriginator The domain name of the administrative originator.
166
+ * @param token The new UMP token to create.
167
+ * @param oldTokenToConsume If provided, the old token that must be consumed in the same transaction.
168
+ * @returns The newly created outpoint.
169
+ */
170
+ buildAndSend: (
171
+ wallet: WalletInterface,
172
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes,
173
+ token: UMPToken,
174
+ oldTokenToConsume?: UMPToken
175
+ ) => Promise<OutpointString>
176
+ }
177
+
178
+ /**
179
+ * @class OverlayUMPTokenInteractor
180
+ *
181
+ * A concrete implementation of the UMPTokenInteractor interface that interacts
182
+ * with Overlay Services and the UMP (User Management Protocol) topic. This class
183
+ * is responsible for:
184
+ *
185
+ * 1) Locating UMP tokens via overlay lookups (ls_users).
186
+ * 2) Creating and publishing new or updated UMP token outputs on-chain under
187
+ * the "tm_users" topic.
188
+ * 3) Consuming (spending) an old token if provided.
189
+ */
190
+ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
191
+ /**
192
+ * A `LookupResolver` instance used to query overlay networks.
193
+ */
194
+ private readonly resolver: LookupResolver
195
+
196
+ /**
197
+ * A SHIP broadcaster that can be used to publish updated UMP tokens
198
+ * under the `tm_users` topic to overlay service peers.
199
+ */
200
+ private readonly broadcaster: SHIPBroadcaster
201
+
202
+ /**
203
+ * Construct a new OverlayUMPTokenInteractor.
204
+ *
205
+ * @param resolver A LookupResolver instance for performing overlay queries (ls_users).
206
+ * @param broadcaster A SHIPBroadcaster instance for sharing new or updated tokens across the `tm_users` overlay.
207
+ */
208
+ constructor(
209
+ resolver: LookupResolver = new LookupResolver(),
210
+ broadcaster: SHIPBroadcaster = new SHIPBroadcaster(['tm_users'])
211
+ ) {
212
+ this.resolver = resolver
213
+ this.broadcaster = broadcaster
214
+ }
215
+
216
+ /**
217
+ * Finds a UMP token on-chain by the given presentation key hash, if it exists.
218
+ * Uses the ls_users overlay service to perform the lookup.
219
+ *
220
+ * @param hash The 32-byte SHA-256 hash of the presentation key.
221
+ * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
222
+ */
223
+ public async findByPresentationKeyHash(hash: number[]): Promise<UMPToken | undefined> {
224
+ // Query ls_users for the given presentationHash
225
+ const question = {
226
+ service: 'ls_users',
227
+ query: { presentationHash: Utils.toHex(hash) }
228
+ }
229
+ const answer = await this.resolver.query(question)
230
+ return this.parseLookupAnswer(answer)
231
+ }
232
+
233
+ /**
234
+ * Finds a UMP token on-chain by the given recovery key hash, if it exists.
235
+ * Uses the ls_users overlay service to perform the lookup.
236
+ *
237
+ * @param hash The 32-byte SHA-256 hash of the recovery key.
238
+ * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
239
+ */
240
+ public async findByRecoveryKeyHash(hash: number[]): Promise<UMPToken | undefined> {
241
+ const question = {
242
+ service: 'ls_users',
243
+ query: { recoveryHash: Utils.toHex(hash) }
244
+ }
245
+ const answer = await this.resolver.query(question)
246
+ return this.parseLookupAnswer(answer)
247
+ }
248
+
249
+ /**
250
+ * Creates or updates (replaces) a UMP token on-chain. If `oldTokenToConsume` is provided,
251
+ * it is spent in the same transaction that creates the new token output. The new token is
252
+ * then broadcast and published under the `tm_users` topic using a SHIP broadcast, ensuring
253
+ * overlay participants see the updated token.
254
+ *
255
+ * @param wallet The wallet used to build and sign the transaction.
256
+ * @param adminOriginator The domain/FQDN of the administrative originator (wallet operator).
257
+ * @param token The new UMPToken to create on-chain.
258
+ * @param oldTokenToConsume Optionally, an existing token to consume/spend in the same transaction.
259
+ * @returns The outpoint of the newly created UMP token (e.g. "abcd1234...ef.0").
260
+ */
261
+ public async buildAndSend(
262
+ wallet: WalletInterface,
263
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes,
264
+ token: UMPToken,
265
+ oldTokenToConsume?: UMPToken
266
+ ): Promise<OutpointString> {
267
+ // 1) Construct the data fields for the new UMP token in the same
268
+ // 11-field order used by the UMP protocol's PushDrop definition.
269
+ const fields: number[][] = new Array(11)
270
+
271
+ // See: UMP field ordering
272
+ // 0 => passwordSalt
273
+ // 1 => passwordPresentationPrimary
274
+ // 2 => passwordRecoveryPrimary
275
+ // 3 => presentationRecoveryPrimary
276
+ // 4 => passwordPrimaryPrivileged
277
+ // 5 => presentationRecoveryPrivileged
278
+ // 6 => presentationHash
279
+ // 7 => recoveryHash
280
+ // 8 => presentationKeyEncrypted
281
+ // 9 => passwordKeyEncrypted
282
+ // 10 => recoveryKeyEncrypted
283
+
284
+ fields[0] = token.passwordSalt
285
+ fields[1] = token.passwordPresentationPrimary
286
+ fields[2] = token.passwordRecoveryPrimary
287
+ fields[3] = token.presentationRecoveryPrimary
288
+ fields[4] = token.passwordPrimaryPrivileged
289
+ fields[5] = token.presentationRecoveryPrivileged
290
+ fields[6] = token.presentationHash
291
+ fields[7] = token.recoveryHash
292
+ fields[8] = token.presentationKeyEncrypted
293
+ fields[9] = token.passwordKeyEncrypted
294
+ fields[10] = token.recoveryKeyEncrypted
295
+
296
+ // 2) Create a PushDrop script referencing these fields, locked with the admin key (for easy revocation).
297
+ const script = await new PushDrop(wallet, adminOriginator).lock(
298
+ fields,
299
+ [2, 'admin user management token'], // protocolID
300
+ '1', // keyID
301
+ 'self', // counterparty
302
+ /*forSelf=*/ true,
303
+ /*includeSignature=*/ true
304
+ )
305
+
306
+ // 3) Prepare the createAction call. If oldTokenToConsume is provided, we gather the outpoint.
307
+ const inputs: CreateActionInput[] = []
308
+ let inputToken: { beef: number[]; outputIndex: number } | undefined
309
+ if (oldTokenToConsume?.currentOutpoint) {
310
+ inputs.push({
311
+ outpoint: oldTokenToConsume.currentOutpoint,
312
+ unlockingScriptLength: 73, // typical signature length
313
+ inputDescription: 'Consume old UMP token'
314
+ })
315
+ inputToken = await this.findByOutpoint(oldTokenToConsume.currentOutpoint)
316
+ }
317
+
318
+ const outputs = [
319
+ {
320
+ lockingScript: script.toHex(),
321
+ satoshis: 1,
322
+ outputDescription: 'New UMP token output'
323
+ }
324
+ ]
325
+
326
+ // 4) Build the partial transaction via createAction.
327
+ const createResult = await wallet.createAction(
328
+ {
329
+ description: oldTokenToConsume ? 'Renew UMP token (consume old, create new)' : 'Create new UMP token',
330
+ inputs,
331
+ outputs,
332
+ inputBEEF: inputToken?.beef
333
+ },
334
+ adminOriginator
335
+ )
336
+
337
+ // If the transaction is fully processed by the wallet (some wallets might do signAndProcess automatically),
338
+ // we retrieve the final TXID from the result.
339
+ if (!createResult.signableTransaction) {
340
+ const finalTxid =
341
+ createResult.txid || (createResult.tx ? Transaction.fromAtomicBEEF(createResult.tx).id('hex') : undefined)
342
+ if (!finalTxid) {
343
+ throw new Error('No signableTransaction and no final TX found.')
344
+ }
345
+ // Now broadcast to `tm_users` using SHIP
346
+ const broadcastTx = Transaction.fromAtomicBEEF(createResult.tx!)
347
+ const facilitator = new HTTPSOverlayBroadcastFacilitator()
348
+ const result = await facilitator.send('https://users.bapp.dev', {
349
+ topics: ['tm_users'],
350
+ beef: broadcastTx.toBEEF()
351
+ })
352
+ await this.broadcaster.broadcast(broadcastTx)
353
+ console.log('BROADCAST RESULT', result)
354
+ return `${finalTxid}.0`
355
+ }
356
+
357
+ // 5) If oldTokenToConsume is present, we must sign the input referencing it.
358
+ // (If there's no old token, there's nothing to sign for the input.)
359
+ let finalTxid = ''
360
+ const reference = createResult.signableTransaction.reference
361
+ const partialTx = Transaction.fromBEEF(createResult.signableTransaction.tx)
362
+
363
+ if (oldTokenToConsume?.currentOutpoint) {
364
+ // Unlock the old token with a matching PushDrop unlocker
365
+ const unlocker = new PushDrop(wallet, adminOriginator).unlock([2, 'admin user management token'], '1', 'self')
366
+ const unlockingScript = await unlocker.sign(partialTx, 0)
367
+
368
+ // Provide it to the wallet
369
+ const signResult = await wallet.signAction(
370
+ {
371
+ reference,
372
+ spends: {
373
+ 0: {
374
+ unlockingScript: unlockingScript.toHex()
375
+ }
376
+ }
377
+ },
378
+ adminOriginator
379
+ )
380
+ finalTxid = signResult.txid || (signResult.tx ? Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '')
381
+ if (!finalTxid) {
382
+ throw new Error('Could not finalize transaction for renewed UMP token.')
383
+ }
384
+ // 6) Broadcast to `tm_users`
385
+ const finalAtomicTx = signResult.tx
386
+ const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx!)
387
+ const facilitator = new HTTPSOverlayBroadcastFacilitator()
388
+ const result = await facilitator.send('https://users.bapp.dev', {
389
+ topics: ['tm_users'],
390
+ beef: broadcastTx.toBEEF()
391
+ })
392
+ console.log('BROADCAST RESULT', result)
393
+ return `${finalTxid}.0`
394
+ } else {
395
+ // Fallbaack
396
+ const signResult = await wallet.signAction({ reference, spends: {} }, adminOriginator)
397
+ finalTxid = signResult.txid || (signResult.tx ? Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '')
398
+ if (!finalTxid) {
399
+ throw new Error('Failed to finalize new UMP token transaction.')
400
+ }
401
+ const finalAtomicTx = signResult.tx
402
+ const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx!)
403
+ const facilitator = new HTTPSOverlayBroadcastFacilitator()
404
+ const result = await facilitator.send('https://users.bapp.dev', {
405
+ topics: ['tm_users'],
406
+ beef: broadcastTx.toBEEF()
407
+ })
408
+ console.log('BROADCAST RESULT', result)
409
+ return `${finalTxid}.0`
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Attempts to parse a LookupAnswer from the UMP lookup service. If successful,
415
+ * extracts the token fields from the resulting transaction and constructs
416
+ * a UMPToken object.
417
+ *
418
+ * @param answer The LookupAnswer returned by a query to ls_users.
419
+ * @returns The parsed UMPToken or `undefined` if none found/decodable.
420
+ */
421
+ private parseLookupAnswer(answer: LookupAnswer): UMPToken | undefined {
422
+ if (answer.type !== 'output-list') {
423
+ return undefined
424
+ }
425
+ if (!answer.outputs || answer.outputs.length === 0) {
426
+ return undefined
427
+ }
428
+
429
+ // We expect only one relevant UMP token in most queries, so let's parse the first.
430
+ // If multiple are returned, we can parse the first.
431
+ const { beef, outputIndex } = answer.outputs[0]
432
+ try {
433
+ const tx = Transaction.fromBEEF(beef)
434
+ const outpoint = `${tx.id('hex')}.${outputIndex}`
435
+
436
+ const decoded = PushDrop.decode(tx.outputs[outputIndex].lockingScript)
437
+
438
+ // Expecting 11 fields for UMP
439
+ if (!decoded.fields || decoded.fields.length < 11) return undefined
440
+
441
+ // Build the UMP token from these fields, preserving outpoint
442
+ const t: UMPToken = {
443
+ passwordSalt: decoded.fields[0],
444
+ passwordPresentationPrimary: decoded.fields[1],
445
+ passwordRecoveryPrimary: decoded.fields[2],
446
+ presentationRecoveryPrimary: decoded.fields[3],
447
+ passwordPrimaryPrivileged: decoded.fields[4],
448
+ presentationRecoveryPrivileged: decoded.fields[5],
449
+ presentationHash: decoded.fields[6],
450
+ recoveryHash: decoded.fields[7],
451
+ presentationKeyEncrypted: decoded.fields[8],
452
+ passwordKeyEncrypted: decoded.fields[9],
453
+ recoveryKeyEncrypted: decoded.fields[10],
454
+ currentOutpoint: outpoint
455
+ }
456
+ return t
457
+ } catch (e) {
458
+ // If we fail to parse or decode, return undefined
459
+ return undefined
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Finds by outpoint for unlocking / spending previous tokens.
465
+ * @param outpoint The outpoint we are searching by
466
+ * @returns The result so that we can use it to unlock the transaction
467
+ */
468
+ private async findByOutpoint(outpoint: string): Promise<{ beef: number[]; outputIndex: number } | undefined> {
469
+ const results = await this.resolver.query({
470
+ service: 'ls_users',
471
+ query: {
472
+ outpoint
473
+ }
474
+ })
475
+ if (results.type !== 'output-list') {
476
+ return undefined
477
+ }
478
+ if (!results.outputs.length) {
479
+ return undefined
480
+ }
481
+ return results.outputs[0]
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Manages a "CWI-style" wallet that uses a UMP token and a
487
+ * multi-key authentication scheme (password, presentation key, and recovery key).
488
+ */
489
+ export class CWIStyleWalletManager implements WalletInterface {
490
+ /**
491
+ * Whether the user is currently authenticated.
492
+ */
493
+ authenticated: boolean
494
+
495
+ /**
496
+ * The domain name of the administrative originator (wallet operator / vendor, or your own).
497
+ */
498
+ private adminOriginator: string
499
+
500
+ /**
501
+ * The system that locates and publishes UMP tokens on-chain.
502
+ */
503
+ private UMPTokenInteractor: UMPTokenInteractor
504
+
505
+ /**
506
+ * A function called to persist the newly generated recovery key.
507
+ * It should generally trigger a UI prompt where the user is asked to write it down.
508
+ */
509
+ private recoveryKeySaver: (key: number[]) => Promise<true>
510
+
511
+ /**
512
+ * Asks the user to enter their password, for a given reason.
513
+ * The test function can be used to see if the password is correct before resolving.
514
+ * Only resolve with the correct password or reject with an error.
515
+ * Resolving with an incorrect password will throw an error.
516
+ */
517
+ private passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>
518
+
519
+ /**
520
+ * An optional function that funds a new Wallet after the new-user flow, before the system proceeds.
521
+ * Allows integration with faucets, and provides the presentation key for use in claiming faucet funds
522
+ * that may be bound to it.
523
+ */
524
+ private newWalletFunder?: (
525
+ presentationKey: number[],
526
+ wallet: WalletInterface,
527
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes
528
+ ) => Promise<void>
529
+
530
+ /**
531
+ * Builds the underlying wallet once the user has been authenticated.
532
+ */
533
+ private walletBuilder: (primaryKey: number[], privilegedKeyManager: PrivilegedKeyManager) => Promise<WalletInterface>
534
+
535
+ /**
536
+ * The current mode of authentication:
537
+ * - 'presentation-key-and-password'
538
+ * - 'presentation-key-and-recovery-key'
539
+ * - 'recovery-key-and-password'
540
+ */
541
+ authenticationMode:
542
+ | 'presentation-key-and-password'
543
+ | 'presentation-key-and-recovery-key'
544
+ | 'recovery-key-and-password' = 'presentation-key-and-password'
545
+
546
+ /**
547
+ * Indicates whether this is a new user or an existing user flow:
548
+ * - 'new-user'
549
+ * - 'existing-user'
550
+ */
551
+ authenticationFlow: 'new-user' | 'existing-user' = 'new-user'
552
+
553
+ /**
554
+ * The current UMP token in use (representing the user's keys on-chain).
555
+ */
556
+ private currentUMPToken?: UMPToken
557
+
558
+ /**
559
+ * The presentation key, temporarily retained after being provided until authenticated.
560
+ */
561
+ private presentationKey?: number[]
562
+
563
+ /**
564
+ * The recovery key, temporarily retained after being provided until authenticated.
565
+ */
566
+ private recoveryKey?: number[]
567
+
568
+ /**
569
+ * The user's primary key, which is used to operate the underlying wallet.
570
+ * It is also stored within state snapshots.
571
+ */
572
+ private primaryKey?: number[]
573
+
574
+ /**
575
+ * The underlying wallet that handles the
576
+ * actual signing, encryption, and other wallet operations.
577
+ */
578
+ private underlying?: WalletInterface
579
+
580
+ /**
581
+ * Privileged key manager associated with the underlying wallet, used for
582
+ * short-term administrative tasks (e.g. re-wrapping or rotating keys).
583
+ */
584
+ private underlyingPrivilegedKeyManager?: PrivilegedKeyManager
585
+
586
+ /**
587
+ * Constructs a new CWIStyleWalletManager.
588
+ *
589
+ * @param adminOriginator The domain name of the administrative originator.
590
+ * @param walletBuilder A function that can build an underlying wallet instance
591
+ * from a primary key and a privileged key manager
592
+ * @param interactor An instance of UMPTokenInteractor capable of managing UMP tokens.
593
+ * @param recoveryKeySaver A function that can persist or display a newly generated recovery key.
594
+ * @param passwordRetriever A function to request the user's password, given a reason and a test function.
595
+ * @param newWalletFunder An optional function called with the presentation key and a new Wallet post-construction to fund it before use.
596
+ * @param stateSnapshot If provided, a previously saved snapshot of the wallet's state.
597
+ */
598
+ constructor(
599
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes,
600
+ walletBuilder: (primaryKey: number[], privilegedKeyManager: PrivilegedKeyManager) => Promise<WalletInterface>,
601
+ interactor: UMPTokenInteractor = new OverlayUMPTokenInteractor(),
602
+ recoveryKeySaver: (key: number[]) => Promise<true>,
603
+ passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>,
604
+ newWalletFunder?: (
605
+ presentationKey: number[],
606
+ wallet: WalletInterface,
607
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes
608
+ ) => Promise<void>,
609
+ stateSnapshot?: number[]
610
+ ) {
611
+ this.adminOriginator = adminOriginator
612
+ this.walletBuilder = walletBuilder
613
+ this.UMPTokenInteractor = interactor
614
+ this.recoveryKeySaver = recoveryKeySaver
615
+ this.passwordRetriever = passwordRetriever
616
+ this.authenticated = false
617
+ this.newWalletFunder = newWalletFunder
618
+
619
+ // If a saved snapshot is provided, attempt to load it.
620
+ if (stateSnapshot) {
621
+ this.loadSnapshot(stateSnapshot)
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Provides the presentation key in an authentication mode that requires it.
627
+ * If a UMP token is found based on the key's hash, this is an existing-user flow.
628
+ * Otherwise, it is treated as a new-user flow.
629
+ *
630
+ * @param key The user's presentation key (32 bytes).
631
+ * @throws {Error} if user is already authenticated, or if the current mode does not require a presentation key.
632
+ */
633
+ async providePresentationKey(key: number[]): Promise<void> {
634
+ if (this.authenticated) {
635
+ throw new Error('User is already authenticated')
636
+ }
637
+ if (this.authenticationMode === 'recovery-key-and-password') {
638
+ throw new Error('Presentation key is not needed in this mode')
639
+ }
640
+
641
+ const hash = Hash.sha256(key)
642
+ const token = await this.UMPTokenInteractor.findByPresentationKeyHash(hash)
643
+
644
+ if (!token) {
645
+ // No token found -> New user
646
+ this.authenticationFlow = 'new-user'
647
+ this.presentationKey = key
648
+ } else {
649
+ // Found token -> existing user
650
+ this.authenticationFlow = 'existing-user'
651
+ this.presentationKey = key
652
+ this.currentUMPToken = token
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Provides the password in an authentication mode that requires it.
658
+ *
659
+ * - **Existing user**:
660
+ * Decrypts the primary key using the provided password (and either the presentation key or recovery key, depending on the mode).
661
+ * Then builds the underlying wallet, marking the user as authenticated.
662
+ *
663
+ * - **New user**:
664
+ * Generates a new UMP token with fresh keys (primary, privileged, recovery). Publishes it on-chain and builds the wallet.
665
+ *
666
+ * @param password The user's password as a string.
667
+ * @throws {Error} If the user is already authenticated, if the mode does not use a password, or if required keys are missing.
668
+ */
669
+ async providePassword(password: string): Promise<void> {
670
+ if (this.authenticated) {
671
+ throw new Error('User is already authenticated')
672
+ }
673
+ if (this.authenticationMode === 'presentation-key-and-recovery-key') {
674
+ throw new Error('Password is not needed in this mode')
675
+ }
676
+
677
+ // If we detect an existing user flow:
678
+ if (this.authenticationFlow === 'existing-user') {
679
+ if (!this.currentUMPToken) {
680
+ throw new Error(
681
+ 'Provide either a presentation key or a recovery key first, depending on the authentication mode.'
682
+ )
683
+ }
684
+ const derivedPasswordKey = Hash.pbkdf2(
685
+ Utils.toArray(password, 'utf8'),
686
+ this.currentUMPToken.passwordSalt,
687
+ PBKDF2_NUM_ROUNDS,
688
+ 32,
689
+ 'sha512'
690
+ )
691
+
692
+ if (this.authenticationMode === 'presentation-key-and-password') {
693
+ if (!this.presentationKey) {
694
+ throw new Error('No presentation key found!')
695
+ }
696
+
697
+ // Decrypt the primary key with XOR(presentationKey, derivedPasswordKey).
698
+ const xorKey = this.XOR(this.presentationKey, derivedPasswordKey)
699
+ const decryptedPrimary = new SymmetricKey(xorKey).decrypt(
700
+ this.currentUMPToken.passwordPresentationPrimary
701
+ ) as number[]
702
+
703
+ await this.buildUnderlying(decryptedPrimary)
704
+ } else {
705
+ // 'recovery-key-and-password' mode
706
+ if (!this.recoveryKey) {
707
+ throw new Error('No recovery key found!')
708
+ }
709
+
710
+ // Decrypt the primary key with XOR(recoveryKey, derivedPasswordKey).
711
+ const primaryDecryptionKey = this.XOR(this.recoveryKey, derivedPasswordKey)
712
+ const decryptedPrimary = new SymmetricKey(primaryDecryptionKey).decrypt(
713
+ this.currentUMPToken.passwordRecoveryPrimary
714
+ ) as number[]
715
+
716
+ // Decrypt the privileged key for immediate use.
717
+ const privilegedDecryptionKey = this.XOR(decryptedPrimary, derivedPasswordKey)
718
+ const decryptedPrivileged = new SymmetricKey(privilegedDecryptionKey).decrypt(
719
+ this.currentUMPToken.passwordPrimaryPrivileged
720
+ ) as number[]
721
+
722
+ await this.buildUnderlying(decryptedPrimary, decryptedPrivileged)
723
+ }
724
+
725
+ return
726
+ }
727
+
728
+ // Otherwise, handle new user flow (only valid in 'presentation-key-and-password').
729
+ if (this.authenticationMode !== 'presentation-key-and-password') {
730
+ throw new Error('New-user flow requires presentation key and password, not recovery key mode.')
731
+ }
732
+
733
+ if (!this.presentationKey) {
734
+ throw new Error('No presentation key provided for new-user flow.')
735
+ }
736
+
737
+ // Generate new random keys/salt and create a new UMP token.
738
+ const recoveryKey = Random(32)
739
+ await this.recoveryKeySaver(recoveryKey)
740
+
741
+ const passwordSalt = Random(32)
742
+ const passwordKey = Hash.pbkdf2(Utils.toArray(password, 'utf8'), passwordSalt, PBKDF2_NUM_ROUNDS, 32, 'sha512')
743
+
744
+ const primaryKey = Random(32)
745
+ const privilegedKey = Random(32)
746
+
747
+ // Build XOR-based symmetrical keys:
748
+ const presentationPassword = new SymmetricKey(this.XOR(this.presentationKey, passwordKey))
749
+ const presentationRecovery = new SymmetricKey(this.XOR(this.presentationKey, recoveryKey))
750
+ const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
751
+ const primaryPassword = new SymmetricKey(this.XOR(primaryKey, passwordKey))
752
+
753
+ // Temporarily create a privileged key manager for encrypting the keys in the token.
754
+ const tempPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(privilegedKey))
755
+
756
+ // Build the new UMP token:
757
+ const newToken: UMPToken = {
758
+ passwordSalt,
759
+ passwordPresentationPrimary: presentationPassword.encrypt(primaryKey) as number[],
760
+ passwordRecoveryPrimary: recoveryPassword.encrypt(primaryKey) as number[],
761
+ presentationRecoveryPrimary: presentationRecovery.encrypt(primaryKey) as number[],
762
+ passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey) as number[],
763
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey) as number[],
764
+ presentationHash: Hash.sha256(this.presentationKey),
765
+ recoveryHash: Hash.sha256(recoveryKey),
766
+ presentationKeyEncrypted: (
767
+ await tempPrivilegedKeyManager.encrypt({
768
+ plaintext: this.presentationKey,
769
+ protocolID: [2, 'admin key wrapping'],
770
+ keyID: '1'
771
+ })
772
+ ).ciphertext,
773
+ passwordKeyEncrypted: (
774
+ await tempPrivilegedKeyManager.encrypt({
775
+ plaintext: passwordKey,
776
+ protocolID: [2, 'admin key wrapping'],
777
+ keyID: '1'
778
+ })
779
+ ).ciphertext,
780
+ recoveryKeyEncrypted: (
781
+ await tempPrivilegedKeyManager.encrypt({
782
+ plaintext: recoveryKey,
783
+ protocolID: [2, 'admin key wrapping'],
784
+ keyID: '1'
785
+ })
786
+ ).ciphertext
787
+ }
788
+
789
+ // Now, we can create our new wallet!
790
+ this.currentUMPToken = newToken
791
+ await this.buildUnderlying(primaryKey)
792
+
793
+ // Before we do anything, the new wallet is most likely empty right now.
794
+ // We want to provide a chance for someone to fund it, if they want.
795
+ if (this.newWalletFunder) {
796
+ try {
797
+ await this.newWalletFunder(this.presentationKey, this.underlying!, this.adminOriginator)
798
+ } catch (e) {
799
+ // swallow error
800
+ // TODO: Implement better error handling
801
+ console.error(e)
802
+ }
803
+ }
804
+
805
+ // Publish the new UMP token on-chain and store the resulting outpoint.
806
+ this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
807
+ this.underlying!,
808
+ this.adminOriginator,
809
+ newToken
810
+ )
811
+ }
812
+
813
+ /**
814
+ * Provides the recovery key in an authentication flow that requires it.
815
+ *
816
+ * @param recoveryKey The user's recovery key (32 bytes).
817
+ * @throws {Error} if user is already authenticated, if the mode does not use a recovery key,
818
+ * or if a required presentation key is missing in "presentation-key-and-recovery-key" mode.
819
+ */
820
+ async provideRecoveryKey(recoveryKey: number[]): Promise<void> {
821
+ if (this.authenticated) {
822
+ throw new Error('Already authenticated')
823
+ }
824
+
825
+ // Cannot use recovery key in a new-user flow
826
+ if (this.authenticationFlow === 'new-user') {
827
+ throw new Error('Do not submit recovery key in new-user flow')
828
+ }
829
+
830
+ if (this.authenticationMode === 'presentation-key-and-password') {
831
+ throw new Error('No recovery key required in this mode')
832
+ } else if (this.authenticationMode === 'recovery-key-and-password') {
833
+ // We will need to wait until the user provides the password as well.
834
+ const hash = Hash.sha256(recoveryKey)
835
+ const token = await this.UMPTokenInteractor.findByRecoveryKeyHash(hash)
836
+ if (!token) {
837
+ throw new Error('No user found with this key')
838
+ }
839
+ this.recoveryKey = recoveryKey
840
+ this.currentUMPToken = token
841
+ } else {
842
+ // 'presentation-key-and-recovery-key'
843
+ if (!this.presentationKey) {
844
+ throw new Error('Provide the presentation key first')
845
+ }
846
+ if (!this.currentUMPToken) {
847
+ throw new Error('Current UMP token not found')
848
+ }
849
+
850
+ // Decrypt the primary key:
851
+ const xorKey = this.XOR(this.presentationKey, recoveryKey)
852
+ const primaryKey = new SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrimary) as number[]
853
+
854
+ // Decrypt the privileged key (for account recovery).
855
+ const privilegedKey = new SymmetricKey(xorKey).decrypt(
856
+ this.currentUMPToken.presentationRecoveryPrivileged
857
+ ) as number[]
858
+
859
+ await this.buildUnderlying(primaryKey, privilegedKey)
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Saves the current wallet state (including the current UMP token and primary key)
865
+ * into an encrypted snapshot. This snapshot can be stored locally and later passed
866
+ * to `loadSnapshot` to restore the wallet state without re-authenticating manually.
867
+ *
868
+ * @remarks
869
+ * Storing the snapshot provides a fully authenticated state.
870
+ * This **must** be securely stored (e.g. system keychain or encrypted file).
871
+ * If attackers gain access to this snapshot, they can fully control the wallet.
872
+ *
873
+ * @returns An array of bytes representing the encrypted snapshot.
874
+ * @throws {Error} if no primary key or token is currently set.
875
+ */
876
+ saveSnapshot(): number[] {
877
+ if (!this.primaryKey || !this.currentUMPToken) {
878
+ throw new Error('No primary key or current UMP token set')
879
+ }
880
+
881
+ // Generate a random snapshot encryption key:
882
+ const snapshotKey = Random(32)
883
+
884
+ // Serialize the relevant data to a preimage buffer:
885
+ const snapshotPreimageWriter = new Utils.Writer()
886
+
887
+ // Write the primary key (32 bytes):
888
+ snapshotPreimageWriter.write(this.primaryKey)
889
+
890
+ // Write the serialized UMP token:
891
+ const serializedToken = this.serializeUMPToken(this.currentUMPToken)
892
+ snapshotPreimageWriter.write(serializedToken)
893
+
894
+ // Encrypt the combined data with the snapshotKey:
895
+ const snapshotPreimage = snapshotPreimageWriter.toArray()
896
+ const snapshotPayload = new SymmetricKey(snapshotKey).encrypt(snapshotPreimage) as number[]
897
+
898
+ // Build the final snapshot structure: [snapshotKey (32 bytes) + encryptedPayload]
899
+ const snapshotWriter = new Utils.Writer()
900
+ snapshotWriter.write(snapshotKey)
901
+ snapshotWriter.write(snapshotPayload)
902
+
903
+ return snapshotWriter.toArray()
904
+ }
905
+
906
+ /**
907
+ * Loads a previously saved state snapshot (e.g. from `saveSnapshot`).
908
+ * Upon success, the wallet becomes authenticated without needing to re-enter keys.
909
+ *
910
+ * @param snapshot An array of bytes that was previously produced by `saveSnapshot`.
911
+ * @throws {Error} If the snapshot format is invalid or decryption fails.
912
+ */
913
+ async loadSnapshot(snapshot: number[]): Promise<void> {
914
+ try {
915
+ const reader = new Utils.Reader(snapshot)
916
+
917
+ // First 32 bytes is the snapshotKey:
918
+ const snapshotKey = reader.read(32)
919
+
920
+ // The rest is the encrypted payload:
921
+ const encryptedPayload = reader.read()
922
+
923
+ // Decrypt the payload:
924
+ const decryptedPayload = new SymmetricKey(snapshotKey).decrypt(encryptedPayload) as number[]
925
+
926
+ const payloadReader = new Utils.Reader(decryptedPayload)
927
+
928
+ // Read the primary key (32 bytes):
929
+ const primaryKey = payloadReader.read(32)
930
+
931
+ // Read the remainder as the serialized UMP token:
932
+ const tokenBytes = payloadReader.read()
933
+ const token = this.deserializeUMPToken(tokenBytes)
934
+
935
+ // Assign and build:
936
+ this.currentUMPToken = token
937
+ await this.buildUnderlying(primaryKey)
938
+ } catch (error) {
939
+ throw new Error(`Failed to load snapshot: ${(error as Error).message}`)
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Destroys the underlying wallet, returning to a default state
945
+ */
946
+ destroy(): void {
947
+ this.underlying = undefined
948
+ this.underlyingPrivilegedKeyManager = undefined
949
+ this.authenticated = false
950
+ this.primaryKey = undefined
951
+ this.currentUMPToken = undefined
952
+ this.presentationKey = undefined
953
+ this.recoveryKey = undefined
954
+ this.authenticationMode = 'presentation-key-and-password'
955
+ this.authenticationFlow = 'new-user'
956
+ }
957
+
958
+ /**
959
+ * Changes the user's password, re-wrapping the primary and privileged keys with the new password factor.
960
+ *
961
+ * @param newPassword The user's new password as a string.
962
+ * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
963
+ */
964
+ async changePassword(newPassword: string): Promise<void> {
965
+ if (!this.authenticated) {
966
+ throw new Error('Not authenticated.')
967
+ }
968
+ if (!this.currentUMPToken) {
969
+ throw new Error('No UMP token to update.')
970
+ }
971
+
972
+ const passwordSalt = Random(32)
973
+ const passwordKey = Hash.pbkdf2(Utils.toArray(newPassword, 'utf8'), passwordSalt, PBKDF2_NUM_ROUNDS, 32, 'sha512')
974
+
975
+ // Decrypt existing factors via the privileged key manager:
976
+ const recoveryKey = (
977
+ await this.underlyingPrivilegedKeyManager!.decrypt({
978
+ ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
979
+ protocolID: [2, 'admin key wrapping'],
980
+ keyID: '1'
981
+ })
982
+ ).plaintext
983
+ const presentationKey = (
984
+ await this.underlyingPrivilegedKeyManager!.decrypt({
985
+ ciphertext: this.currentUMPToken.presentationKeyEncrypted,
986
+ protocolID: [2, 'admin key wrapping'],
987
+ keyID: '1'
988
+ })
989
+ ).plaintext
990
+ const privilegedKey = new SymmetricKey(this.XOR(presentationKey, recoveryKey)).decrypt(
991
+ this.currentUMPToken.presentationRecoveryPrivileged
992
+ ) as number[]
993
+
994
+ await this.updateAuthFactors(
995
+ passwordSalt,
996
+ passwordKey,
997
+ presentationKey,
998
+ recoveryKey,
999
+ this.primaryKey!,
1000
+ privilegedKey
1001
+ )
1002
+ }
1003
+
1004
+ /**
1005
+ * Retrieves the current recovery key.
1006
+ *
1007
+ * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
1008
+ */
1009
+ async getRecoveryKey(): Promise<number[]> {
1010
+ if (!this.authenticated) {
1011
+ throw new Error('Not authenticated.')
1012
+ }
1013
+ if (!this.currentUMPToken) {
1014
+ throw new Error('No UMP token!')
1015
+ }
1016
+ return (
1017
+ await this.underlyingPrivilegedKeyManager!.decrypt({
1018
+ ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
1019
+ protocolID: [2, 'admin key wrapping'],
1020
+ keyID: '1'
1021
+ })
1022
+ ).plaintext
1023
+ }
1024
+
1025
+ /**
1026
+ * Changes the user's recovery key, prompting the user to save the new key.
1027
+ *
1028
+ * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
1029
+ */
1030
+ async changeRecoveryKey(): Promise<void> {
1031
+ if (!this.authenticated) {
1032
+ throw new Error('Not authenticated.')
1033
+ }
1034
+ if (!this.currentUMPToken) {
1035
+ throw new Error('No UMP token to update.')
1036
+ }
1037
+
1038
+ // Decrypt existing password/presentation keys via the privileged key manager:
1039
+ const passwordKey = (
1040
+ await this.underlyingPrivilegedKeyManager!.decrypt({
1041
+ ciphertext: this.currentUMPToken.passwordKeyEncrypted,
1042
+ protocolID: [2, 'admin key wrapping'],
1043
+ keyID: '1'
1044
+ })
1045
+ ).plaintext
1046
+ const presentationKey = (
1047
+ await this.underlyingPrivilegedKeyManager!.decrypt({
1048
+ ciphertext: this.currentUMPToken.presentationKeyEncrypted,
1049
+ protocolID: [2, 'admin key wrapping'],
1050
+ keyID: '1'
1051
+ })
1052
+ ).plaintext
1053
+ const privilegedKey = new SymmetricKey(this.XOR(passwordKey, this.primaryKey!)).decrypt(
1054
+ this.currentUMPToken.passwordPrimaryPrivileged
1055
+ ) as number[]
1056
+
1057
+ const recoveryKey = Random(32)
1058
+ await this.recoveryKeySaver(recoveryKey)
1059
+
1060
+ await this.updateAuthFactors(
1061
+ this.currentUMPToken.passwordSalt,
1062
+ passwordKey,
1063
+ presentationKey,
1064
+ recoveryKey,
1065
+ this.primaryKey!,
1066
+ privilegedKey
1067
+ )
1068
+ }
1069
+
1070
+ /**
1071
+ * Changes the user's presentation key.
1072
+ *
1073
+ * @param presentationKey The new presentation key (32 bytes).
1074
+ * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
1075
+ */
1076
+ async changePresentationKey(presentationKey: number[]): Promise<void> {
1077
+ if (!this.authenticated) {
1078
+ throw new Error('Not authenticated.')
1079
+ }
1080
+ if (!this.currentUMPToken) {
1081
+ throw new Error('No UMP token to update.')
1082
+ }
1083
+
1084
+ // Decrypt existing password/recovery keys via the privileged key manager:
1085
+ const recoveryKey = (
1086
+ await this.underlyingPrivilegedKeyManager!.decrypt({
1087
+ ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
1088
+ protocolID: [2, 'admin key wrapping'],
1089
+ keyID: '1'
1090
+ })
1091
+ ).plaintext
1092
+ const passwordKey = (
1093
+ await this.underlyingPrivilegedKeyManager!.decrypt({
1094
+ ciphertext: this.currentUMPToken.passwordKeyEncrypted,
1095
+ protocolID: [2, 'admin key wrapping'],
1096
+ keyID: '1'
1097
+ })
1098
+ ).plaintext
1099
+ const privilegedKey = new SymmetricKey(this.XOR(passwordKey, this.primaryKey!)).decrypt(
1100
+ this.currentUMPToken.passwordPrimaryPrivileged
1101
+ ) as number[]
1102
+
1103
+ await this.updateAuthFactors(
1104
+ this.currentUMPToken.passwordSalt,
1105
+ passwordKey,
1106
+ presentationKey,
1107
+ recoveryKey,
1108
+ this.primaryKey!,
1109
+ privilegedKey
1110
+ )
1111
+ }
1112
+
1113
+ /**
1114
+ * Internal helper to recompute a UMP token with updated authentication factors and consume the old token on-chain.
1115
+ *
1116
+ * @param passwordSalt The PBKDF2 salt for the new password factor.
1117
+ * @param passwordKey The PBKDF2-derived password key (32 bytes).
1118
+ * @param presentationKey The new or existing presentation key (32 bytes).
1119
+ * @param recoveryKey The new or existing recovery key (32 bytes).
1120
+ * @param primaryKey The user's primary key for re-wrapping.
1121
+ * @param privilegedKey The user's privileged key for re-wrapping.
1122
+ * @throws {Error} If the user is not authenticated or if keys are unavailable.
1123
+ */
1124
+ private async updateAuthFactors(
1125
+ passwordSalt: number[],
1126
+ passwordKey: number[],
1127
+ presentationKey: number[],
1128
+ recoveryKey: number[],
1129
+ primaryKey: number[],
1130
+ privilegedKey: number[]
1131
+ ): Promise<void> {
1132
+ if (!this.authenticated || !this.primaryKey || !this.currentUMPToken) {
1133
+ throw new Error('Wallet is not properly authenticated or missing data.')
1134
+ }
1135
+
1136
+ // Derive symmetrical encryption keys via XOR:
1137
+ const presentationPassword = new SymmetricKey(this.XOR(presentationKey, passwordKey))
1138
+ const presentationRecovery = new SymmetricKey(this.XOR(presentationKey, recoveryKey))
1139
+ const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
1140
+ const primaryPassword = new SymmetricKey(this.XOR(this.primaryKey, passwordKey))
1141
+
1142
+ // Build a temporary privileged key manager just to encrypt the new fields:
1143
+ const tempPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(privilegedKey))
1144
+
1145
+ // Construct the new UMP token:
1146
+ const newToken: UMPToken = {
1147
+ passwordSalt,
1148
+ passwordPresentationPrimary: presentationPassword.encrypt(this.primaryKey) as number[],
1149
+ passwordRecoveryPrimary: recoveryPassword.encrypt(this.primaryKey) as number[],
1150
+ presentationRecoveryPrimary: presentationRecovery.encrypt(this.primaryKey) as number[],
1151
+ passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey) as number[],
1152
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey) as number[],
1153
+ presentationHash: Hash.sha256(presentationKey),
1154
+ recoveryHash: Hash.sha256(recoveryKey),
1155
+ presentationKeyEncrypted: (
1156
+ await tempPrivilegedKeyManager.encrypt({
1157
+ plaintext: presentationKey,
1158
+ protocolID: [2, 'admin key wrapping'],
1159
+ keyID: '1'
1160
+ })
1161
+ ).ciphertext,
1162
+ passwordKeyEncrypted: (
1163
+ await tempPrivilegedKeyManager.encrypt({
1164
+ plaintext: passwordKey,
1165
+ protocolID: [2, 'admin key wrapping'],
1166
+ keyID: '1'
1167
+ })
1168
+ ).ciphertext,
1169
+ recoveryKeyEncrypted: (
1170
+ await tempPrivilegedKeyManager.encrypt({
1171
+ plaintext: recoveryKey,
1172
+ protocolID: [2, 'admin key wrapping'],
1173
+ keyID: '1'
1174
+ })
1175
+ ).ciphertext
1176
+ }
1177
+
1178
+ // Publish the new token on-chain and consume the old one:
1179
+ newToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
1180
+ this.underlying!,
1181
+ this.adminOriginator,
1182
+ newToken,
1183
+ this.currentUMPToken
1184
+ )
1185
+ this.currentUMPToken = newToken
1186
+ }
1187
+
1188
+ /**
1189
+ * A helper function to XOR two equal-length byte arrays.
1190
+ *
1191
+ * @param n1 The first byte array.
1192
+ * @param n2 The second byte array.
1193
+ * @returns A new byte array which is the element-wise XOR of the two inputs.
1194
+ * @throws {Error} if the two arrays are not the same length.
1195
+ */
1196
+ private XOR(n1: number[], n2: number[]): number[] {
1197
+ if (n1.length !== n2.length) {
1198
+ throw new Error('lengths mismatch')
1199
+ }
1200
+ const r = new Array<number>(n1.length)
1201
+ for (let i = 0; i < n1.length; i++) {
1202
+ r[i] = n1[i] ^ n2[i]
1203
+ }
1204
+ return r
1205
+ }
1206
+
1207
+ /**
1208
+ * A helper function to serialize a UMP token to a binary format (version=1).
1209
+ * The serialization layout is:
1210
+ * - [1 byte version (value=1)]
1211
+ * - For each array field in the UMP token, [varint length + bytes]
1212
+ * - Then [varint length + outpoint string in UTF-8]
1213
+ *
1214
+ * @param token The UMP token to serialize.
1215
+ * @returns A byte array representing the serialized token.
1216
+ * @throws {Error} if the token has no currentOutpoint (required for serialization).
1217
+ */
1218
+ private serializeUMPToken(token: UMPToken): number[] {
1219
+ if (!token.currentOutpoint) {
1220
+ throw new Error('Token must have outpoint for serialization')
1221
+ }
1222
+
1223
+ const writer = new Utils.Writer()
1224
+ // Write version byte
1225
+ writer.writeUInt8(1)
1226
+
1227
+ // Helper to write array with length prefix
1228
+ const writeArray = (arr: number[]) => {
1229
+ writer.writeVarIntNum(arr.length)
1230
+ writer.write(arr)
1231
+ }
1232
+
1233
+ // Write each array-based field in the order they appear on UMPToken
1234
+ writeArray(token.passwordPresentationPrimary)
1235
+ writeArray(token.passwordRecoveryPrimary)
1236
+ writeArray(token.presentationRecoveryPrimary)
1237
+ writeArray(token.passwordPrimaryPrivileged)
1238
+ writeArray(token.presentationRecoveryPrivileged)
1239
+ writeArray(token.presentationHash)
1240
+ writeArray(token.passwordSalt)
1241
+ writeArray(token.recoveryHash)
1242
+ writeArray(token.presentationKeyEncrypted)
1243
+ writeArray(token.recoveryKeyEncrypted)
1244
+ writeArray(token.passwordKeyEncrypted)
1245
+
1246
+ // Finally, write the outpoint string:
1247
+ const outpointBytes = Utils.toArray(token.currentOutpoint, 'utf8')
1248
+ writer.writeVarIntNum(outpointBytes.length)
1249
+ writer.write(outpointBytes)
1250
+
1251
+ return writer.toArray()
1252
+ }
1253
+
1254
+ /**
1255
+ * A helper function to deserialize a UMP token from the format described in `serializeUMPToken`.
1256
+ *
1257
+ * @param bin The serialized byte array.
1258
+ * @returns The reconstructed UMP token.
1259
+ * @throws {Error} if the version byte is unexpected or if parsing fails.
1260
+ */
1261
+ private deserializeUMPToken(bin: number[]): UMPToken {
1262
+ const reader = new Utils.Reader(bin)
1263
+
1264
+ // Check version:
1265
+ const version = reader.readUInt8()
1266
+ if (version !== 1) {
1267
+ throw new Error(`Unsupported UMP token version: ${version}`)
1268
+ }
1269
+
1270
+ // Helper to read an array with length prefix
1271
+ const readArray = (): number[] => {
1272
+ const length = reader.readVarIntNum()
1273
+ return reader.read(length)
1274
+ }
1275
+
1276
+ // Read in the correct order:
1277
+ const passwordPresentationPrimary = readArray()
1278
+ const passwordRecoveryPrimary = readArray()
1279
+ const presentationRecoveryPrimary = readArray()
1280
+ const passwordPrimaryPrivileged = readArray()
1281
+ const presentationRecoveryPrivileged = readArray()
1282
+ const presentationHash = readArray()
1283
+ const passwordSalt = readArray()
1284
+ const recoveryHash = readArray()
1285
+ const presentationKeyEncrypted = readArray()
1286
+ const recoveryKeyEncrypted = readArray()
1287
+ const passwordKeyEncrypted = readArray()
1288
+
1289
+ // Read outpoint string:
1290
+ const outpointLen = reader.readVarIntNum()
1291
+ const outpointBytes = reader.read(outpointLen)
1292
+ const currentOutpoint = Utils.toUTF8(outpointBytes)
1293
+
1294
+ const token: UMPToken = {
1295
+ passwordPresentationPrimary,
1296
+ passwordRecoveryPrimary,
1297
+ presentationRecoveryPrimary,
1298
+ passwordPrimaryPrivileged,
1299
+ presentationRecoveryPrivileged,
1300
+ presentationHash,
1301
+ passwordSalt,
1302
+ recoveryHash,
1303
+ presentationKeyEncrypted,
1304
+ recoveryKeyEncrypted,
1305
+ passwordKeyEncrypted,
1306
+ currentOutpoint
1307
+ }
1308
+
1309
+ return token
1310
+ }
1311
+
1312
+ /**
1313
+ * Builds the underlying wallet once the user is authenticated.
1314
+ *
1315
+ * @param primaryKey The user's primary key (32 bytes).
1316
+ * @param privilegedKey Optionally, a privileged key (for short-term usage in account recovery).
1317
+ */
1318
+ private async buildUnderlying(primaryKey: number[], privilegedKey?: number[]): Promise<void> {
1319
+ if (!this.currentUMPToken) {
1320
+ throw new Error('A UMP token must exist before building underlying wallet!')
1321
+ }
1322
+
1323
+ this.primaryKey = primaryKey
1324
+
1325
+ // Create a privileged manager that either uses the ephemeral privilegedKey if provided,
1326
+ // or derives it later from the user's password on demand.
1327
+ const privilegedManager = new PrivilegedKeyManager(async (reason: string) => {
1328
+ if (privilegedKey) {
1329
+ // For account recovery: a one-off opportunity to recover.
1330
+ const tempKey = new PrivateKey(privilegedKey)
1331
+ privilegedKey = undefined
1332
+ return tempKey
1333
+ }
1334
+ // Otherwise, ask user for their password to decrypt the privileged key.
1335
+ const password = await this.passwordRetriever(reason, (passwordCandidate: string) => {
1336
+ try {
1337
+ const derivedPasswordKey = Hash.pbkdf2(
1338
+ Utils.toArray(passwordCandidate, 'utf8'),
1339
+ this.currentUMPToken!.passwordSalt,
1340
+ PBKDF2_NUM_ROUNDS,
1341
+ 32,
1342
+ 'sha512'
1343
+ )
1344
+ // Decrypt the privileged key with XOR(primaryKey, derivedPasswordKey).
1345
+ const privilegedDecryptor = this.XOR(this.primaryKey!, derivedPasswordKey)
1346
+ const decryptedPrivileged = new SymmetricKey(privilegedDecryptor).decrypt(
1347
+ this.currentUMPToken!.passwordPrimaryPrivileged
1348
+ ) as number[]
1349
+ if (decryptedPrivileged) {
1350
+ return true
1351
+ }
1352
+ return false
1353
+ } catch (e) {
1354
+ return false
1355
+ }
1356
+ })
1357
+ const derivedPasswordKey = Hash.pbkdf2(
1358
+ Utils.toArray(password, 'utf8'),
1359
+ this.currentUMPToken!.passwordSalt,
1360
+ PBKDF2_NUM_ROUNDS,
1361
+ 32,
1362
+ 'sha512'
1363
+ )
1364
+ // Decrypt the privileged key with XOR(primaryKey, derivedPasswordKey).
1365
+ const privilegedDecryptor = this.XOR(this.primaryKey!, derivedPasswordKey)
1366
+ const decryptedPrivileged = new SymmetricKey(privilegedDecryptor).decrypt(
1367
+ this.currentUMPToken!.passwordPrimaryPrivileged
1368
+ ) as number[]
1369
+ return new PrivateKey(decryptedPrivileged)
1370
+ })
1371
+
1372
+ this.underlyingPrivilegedKeyManager = privilegedManager
1373
+
1374
+ // Build the underlying wallet with the primary key and privileged manager.
1375
+ this.underlying = await this.walletBuilder(primaryKey, privilegedManager)
1376
+
1377
+ this.authenticated = true
1378
+ }
1379
+
1380
+ /*
1381
+ * ---------------------------------------------------------------------------------------
1382
+ * Below are the standard WalletInterface methods that simply proxy through to this.underlying,
1383
+ * ensuring that the user is authenticated and that the admin originator is not misused.
1384
+ * ---------------------------------------------------------------------------------------
1385
+ */
1386
+
1387
+ async getPublicKey(
1388
+ args: GetPublicKeyArgs,
1389
+ originator?: OriginatorDomainNameStringUnder250Bytes
1390
+ ): Promise<GetPublicKeyResult> {
1391
+ if (!this.authenticated) {
1392
+ throw new Error('User is not authenticated.')
1393
+ }
1394
+ if (originator === this.adminOriginator) {
1395
+ throw new Error('External applications are not allowed to use the admin originator.')
1396
+ }
1397
+ return this.underlying!.getPublicKey(args, originator)
1398
+ }
1399
+
1400
+ async revealCounterpartyKeyLinkage(
1401
+ args: RevealCounterpartyKeyLinkageArgs,
1402
+ originator?: OriginatorDomainNameStringUnder250Bytes
1403
+ ): Promise<RevealCounterpartyKeyLinkageResult> {
1404
+ if (!this.authenticated) {
1405
+ throw new Error('User is not authenticated.')
1406
+ }
1407
+ if (originator === this.adminOriginator) {
1408
+ throw new Error('External applications are not allowed to use the admin originator.')
1409
+ }
1410
+ return this.underlying!.revealCounterpartyKeyLinkage(args, originator)
1411
+ }
1412
+
1413
+ async revealSpecificKeyLinkage(
1414
+ args: RevealSpecificKeyLinkageArgs,
1415
+ originator?: OriginatorDomainNameStringUnder250Bytes
1416
+ ): Promise<RevealSpecificKeyLinkageResult> {
1417
+ if (!this.authenticated) {
1418
+ throw new Error('User is not authenticated.')
1419
+ }
1420
+ if (originator === this.adminOriginator) {
1421
+ throw new Error('External applications are not allowed to use the admin originator.')
1422
+ }
1423
+ return this.underlying!.revealSpecificKeyLinkage(args, originator)
1424
+ }
1425
+
1426
+ async encrypt(
1427
+ args: WalletEncryptArgs,
1428
+ originator?: OriginatorDomainNameStringUnder250Bytes
1429
+ ): Promise<WalletEncryptResult> {
1430
+ if (!this.authenticated) {
1431
+ throw new Error('User is not authenticated.')
1432
+ }
1433
+ if (originator === this.adminOriginator) {
1434
+ throw new Error('External applications are not allowed to use the admin originator.')
1435
+ }
1436
+ return this.underlying!.encrypt(args, originator)
1437
+ }
1438
+
1439
+ async decrypt(
1440
+ args: WalletDecryptArgs,
1441
+ originator?: OriginatorDomainNameStringUnder250Bytes
1442
+ ): Promise<WalletDecryptResult> {
1443
+ if (!this.authenticated) {
1444
+ throw new Error('User is not authenticated.')
1445
+ }
1446
+ if (originator === this.adminOriginator) {
1447
+ throw new Error('External applications are not allowed to use the admin originator.')
1448
+ }
1449
+ return this.underlying!.decrypt(args, originator)
1450
+ }
1451
+
1452
+ async createHmac(
1453
+ args: CreateHmacArgs,
1454
+ originator?: OriginatorDomainNameStringUnder250Bytes
1455
+ ): Promise<CreateHmacResult> {
1456
+ if (!this.authenticated) {
1457
+ throw new Error('User is not authenticated.')
1458
+ }
1459
+ if (originator === this.adminOriginator) {
1460
+ throw new Error('External applications are not allowed to use the admin originator.')
1461
+ }
1462
+ return this.underlying!.createHmac(args, originator)
1463
+ }
1464
+
1465
+ async verifyHmac(
1466
+ args: VerifyHmacArgs,
1467
+ originator?: OriginatorDomainNameStringUnder250Bytes
1468
+ ): Promise<VerifyHmacResult> {
1469
+ if (!this.authenticated) {
1470
+ throw new Error('User is not authenticated.')
1471
+ }
1472
+ if (originator === this.adminOriginator) {
1473
+ throw new Error('External applications are not allowed to use the admin originator.')
1474
+ }
1475
+ return this.underlying!.verifyHmac(args, originator)
1476
+ }
1477
+
1478
+ async createSignature(
1479
+ args: CreateSignatureArgs,
1480
+ originator?: OriginatorDomainNameStringUnder250Bytes
1481
+ ): Promise<CreateSignatureResult> {
1482
+ if (!this.authenticated) {
1483
+ throw new Error('User is not authenticated.')
1484
+ }
1485
+ if (originator === this.adminOriginator) {
1486
+ throw new Error('External applications are not allowed to use the admin originator.')
1487
+ }
1488
+ return this.underlying!.createSignature(args, originator)
1489
+ }
1490
+
1491
+ async verifySignature(
1492
+ args: VerifySignatureArgs,
1493
+ originator?: OriginatorDomainNameStringUnder250Bytes
1494
+ ): Promise<VerifySignatureResult> {
1495
+ if (!this.authenticated) {
1496
+ throw new Error('User is not authenticated.')
1497
+ }
1498
+ if (originator === this.adminOriginator) {
1499
+ throw new Error('External applications are not allowed to use the admin originator.')
1500
+ }
1501
+ return this.underlying!.verifySignature(args, originator)
1502
+ }
1503
+
1504
+ async createAction(
1505
+ args: CreateActionArgs,
1506
+ originator?: OriginatorDomainNameStringUnder250Bytes
1507
+ ): Promise<CreateActionResult> {
1508
+ if (!this.authenticated) {
1509
+ throw new Error('User is not authenticated.')
1510
+ }
1511
+ if (originator === this.adminOriginator) {
1512
+ throw new Error('External applications are not allowed to use the admin originator.')
1513
+ }
1514
+ return this.underlying!.createAction(args, originator)
1515
+ }
1516
+
1517
+ async signAction(
1518
+ args: SignActionArgs,
1519
+ originator?: OriginatorDomainNameStringUnder250Bytes
1520
+ ): Promise<SignActionResult> {
1521
+ if (!this.authenticated) {
1522
+ throw new Error('User is not authenticated.')
1523
+ }
1524
+ if (originator === this.adminOriginator) {
1525
+ throw new Error('External applications are not allowed to use the admin originator.')
1526
+ }
1527
+ return this.underlying!.signAction(args, originator)
1528
+ }
1529
+
1530
+ async abortAction(
1531
+ args: AbortActionArgs,
1532
+ originator?: OriginatorDomainNameStringUnder250Bytes
1533
+ ): Promise<AbortActionResult> {
1534
+ if (!this.authenticated) {
1535
+ throw new Error('User is not authenticated.')
1536
+ }
1537
+ if (originator === this.adminOriginator) {
1538
+ throw new Error('External applications are not allowed to use the admin originator.')
1539
+ }
1540
+ return this.underlying!.abortAction(args, originator)
1541
+ }
1542
+
1543
+ async listActions(
1544
+ args: ListActionsArgs,
1545
+ originator?: OriginatorDomainNameStringUnder250Bytes
1546
+ ): Promise<ListActionsResult> {
1547
+ if (!this.authenticated) {
1548
+ throw new Error('User is not authenticated.')
1549
+ }
1550
+ if (originator === this.adminOriginator) {
1551
+ throw new Error('External applications are not allowed to use the admin originator.')
1552
+ }
1553
+ return this.underlying!.listActions(args, originator)
1554
+ }
1555
+
1556
+ async internalizeAction(
1557
+ args: InternalizeActionArgs,
1558
+ originator?: OriginatorDomainNameStringUnder250Bytes
1559
+ ): Promise<InternalizeActionResult> {
1560
+ if (!this.authenticated) {
1561
+ throw new Error('User is not authenticated.')
1562
+ }
1563
+ if (originator === this.adminOriginator) {
1564
+ throw new Error('External applications are not allowed to use the admin originator.')
1565
+ }
1566
+ return this.underlying!.internalizeAction(args, originator)
1567
+ }
1568
+
1569
+ async listOutputs(
1570
+ args: ListOutputsArgs,
1571
+ originator?: OriginatorDomainNameStringUnder250Bytes
1572
+ ): Promise<ListOutputsResult> {
1573
+ if (!this.authenticated) {
1574
+ throw new Error('User is not authenticated.')
1575
+ }
1576
+ if (originator === this.adminOriginator) {
1577
+ throw new Error('External applications are not allowed to use the admin originator.')
1578
+ }
1579
+ return this.underlying!.listOutputs(args, originator)
1580
+ }
1581
+
1582
+ async relinquishOutput(
1583
+ args: RelinquishOutputArgs,
1584
+ originator?: OriginatorDomainNameStringUnder250Bytes
1585
+ ): Promise<RelinquishOutputResult> {
1586
+ if (!this.authenticated) {
1587
+ throw new Error('User is not authenticated.')
1588
+ }
1589
+ if (originator === this.adminOriginator) {
1590
+ throw new Error('External applications are not allowed to use the admin originator.')
1591
+ }
1592
+ return this.underlying!.relinquishOutput(args, originator)
1593
+ }
1594
+
1595
+ async acquireCertificate(
1596
+ args: AcquireCertificateArgs,
1597
+ originator?: OriginatorDomainNameStringUnder250Bytes
1598
+ ): Promise<AcquireCertificateResult> {
1599
+ if (!this.authenticated) {
1600
+ throw new Error('User is not authenticated.')
1601
+ }
1602
+ if (originator === this.adminOriginator) {
1603
+ throw new Error('External applications are not allowed to use the admin originator.')
1604
+ }
1605
+ return this.underlying!.acquireCertificate(args, originator)
1606
+ }
1607
+
1608
+ async listCertificates(
1609
+ args: ListCertificatesArgs,
1610
+ originator?: OriginatorDomainNameStringUnder250Bytes
1611
+ ): Promise<ListCertificatesResult> {
1612
+ if (!this.authenticated) {
1613
+ throw new Error('User is not authenticated.')
1614
+ }
1615
+ if (originator === this.adminOriginator) {
1616
+ throw new Error('External applications are not allowed to use the admin originator.')
1617
+ }
1618
+ return this.underlying!.listCertificates(args, originator)
1619
+ }
1620
+
1621
+ async proveCertificate(
1622
+ args: ProveCertificateArgs,
1623
+ originator?: OriginatorDomainNameStringUnder250Bytes
1624
+ ): Promise<ProveCertificateResult> {
1625
+ if (!this.authenticated) {
1626
+ throw new Error('User is not authenticated.')
1627
+ }
1628
+ if (originator === this.adminOriginator) {
1629
+ throw new Error('External applications are not allowed to use the admin originator.')
1630
+ }
1631
+ return this.underlying!.proveCertificate(args, originator)
1632
+ }
1633
+
1634
+ async relinquishCertificate(
1635
+ args: RelinquishCertificateArgs,
1636
+ originator?: OriginatorDomainNameStringUnder250Bytes
1637
+ ): Promise<RelinquishCertificateResult> {
1638
+ if (!this.authenticated) {
1639
+ throw new Error('User is not authenticated.')
1640
+ }
1641
+ if (originator === this.adminOriginator) {
1642
+ throw new Error('External applications are not allowed to use the admin originator.')
1643
+ }
1644
+ return this.underlying!.relinquishCertificate(args, originator)
1645
+ }
1646
+
1647
+ async discoverByIdentityKey(
1648
+ args: DiscoverByIdentityKeyArgs,
1649
+ originator?: OriginatorDomainNameStringUnder250Bytes
1650
+ ): Promise<DiscoverCertificatesResult> {
1651
+ if (!this.authenticated) {
1652
+ throw new Error('User is not authenticated.')
1653
+ }
1654
+ if (originator === this.adminOriginator) {
1655
+ throw new Error('External applications are not allowed to use the admin originator.')
1656
+ }
1657
+ return this.underlying!.discoverByIdentityKey(args, originator)
1658
+ }
1659
+
1660
+ async discoverByAttributes(
1661
+ args: DiscoverByAttributesArgs,
1662
+ originator?: OriginatorDomainNameStringUnder250Bytes
1663
+ ): Promise<DiscoverCertificatesResult> {
1664
+ if (!this.authenticated) {
1665
+ throw new Error('User is not authenticated.')
1666
+ }
1667
+ if (originator === this.adminOriginator) {
1668
+ throw new Error('External applications are not allowed to use the admin originator.')
1669
+ }
1670
+ return this.underlying!.discoverByAttributes(args, originator)
1671
+ }
1672
+
1673
+ async isAuthenticated(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<AuthenticatedResult> {
1674
+ if (!this.authenticated) {
1675
+ throw new Error('User is not authenticated.')
1676
+ }
1677
+ if (originator === this.adminOriginator) {
1678
+ throw new Error('External applications are not allowed to use the admin originator.')
1679
+ }
1680
+ return { authenticated: true }
1681
+ }
1682
+
1683
+ async waitForAuthentication(
1684
+ _: {},
1685
+ originator?: OriginatorDomainNameStringUnder250Bytes
1686
+ ): Promise<AuthenticatedResult> {
1687
+ if (originator === this.adminOriginator) {
1688
+ throw new Error('External applications are not allowed to use the admin originator.')
1689
+ }
1690
+ while (!this.authenticated) {
1691
+ await new Promise(resolve => setTimeout(resolve, 100))
1692
+ }
1693
+ return { authenticated: true }
1694
+ }
1695
+
1696
+ async getHeight(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetHeightResult> {
1697
+ if (!this.authenticated) {
1698
+ throw new Error('User is not authenticated.')
1699
+ }
1700
+ if (originator === this.adminOriginator) {
1701
+ throw new Error('External applications are not allowed to use the admin originator.')
1702
+ }
1703
+ return this.underlying!.getHeight({}, originator)
1704
+ }
1705
+
1706
+ async getHeaderForHeight(
1707
+ args: GetHeaderArgs,
1708
+ originator?: OriginatorDomainNameStringUnder250Bytes
1709
+ ): Promise<GetHeaderResult> {
1710
+ if (!this.authenticated) {
1711
+ throw new Error('User is not authenticated.')
1712
+ }
1713
+ if (originator === this.adminOriginator) {
1714
+ throw new Error('External applications are not allowed to use the admin originator.')
1715
+ }
1716
+ return this.underlying!.getHeaderForHeight(args, originator)
1717
+ }
1718
+
1719
+ async getNetwork(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetNetworkResult> {
1720
+ if (!this.authenticated) {
1721
+ throw new Error('User is not authenticated.')
1722
+ }
1723
+ if (originator === this.adminOriginator) {
1724
+ throw new Error('External applications are not allowed to use the admin originator.')
1725
+ }
1726
+ return this.underlying!.getNetwork({}, originator)
1727
+ }
1728
+
1729
+ async getVersion(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetVersionResult> {
1730
+ if (!this.authenticated) {
1731
+ throw new Error('User is not authenticated.')
1732
+ }
1733
+ if (originator === this.adminOriginator) {
1734
+ throw new Error('External applications are not allowed to use the admin originator.')
1735
+ }
1736
+ return this.underlying!.getVersion({}, originator)
1737
+ }
1738
+ }