@bopen-io/wallet-toolbox 1.7.18

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 (390) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.env.template +22 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  4. package/.github/ISSUE_TEMPLATE/discussion.md +24 -0
  5. package/.github/pull_request_template.md +22 -0
  6. package/.github/workflows/push.yaml +145 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +280 -0
  9. package/CONTRIBUTING.md +89 -0
  10. package/README.md +43 -0
  11. package/docs/README.md +85 -0
  12. package/docs/client.md +19627 -0
  13. package/docs/monitor.md +953 -0
  14. package/docs/open-rpc/index.html +46 -0
  15. package/docs/services.md +6377 -0
  16. package/docs/setup.md +1268 -0
  17. package/docs/storage.md +5367 -0
  18. package/docs/wallet.md +19626 -0
  19. package/jest.config.ts +25 -0
  20. package/license.md +28 -0
  21. package/out/tsconfig.all.tsbuildinfo +1 -0
  22. package/package.json +63 -0
  23. package/src/CWIStyleWalletManager.ts +1999 -0
  24. package/src/Setup.ts +579 -0
  25. package/src/SetupClient.ts +322 -0
  26. package/src/SetupWallet.ts +108 -0
  27. package/src/SimpleWalletManager.ts +526 -0
  28. package/src/Wallet.ts +1169 -0
  29. package/src/WalletAuthenticationManager.ts +153 -0
  30. package/src/WalletLogger.ts +213 -0
  31. package/src/WalletPermissionsManager.ts +3660 -0
  32. package/src/WalletSettingsManager.ts +114 -0
  33. package/src/__tests/CWIStyleWalletManager.test.d.ts.map +1 -0
  34. package/src/__tests/CWIStyleWalletManager.test.js.map +1 -0
  35. package/src/__tests/CWIStyleWalletManager.test.ts +675 -0
  36. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +323 -0
  37. package/src/__tests/WalletPermissionsManager.checks.test.ts +844 -0
  38. package/src/__tests/WalletPermissionsManager.encryption.test.ts +412 -0
  39. package/src/__tests/WalletPermissionsManager.fixtures.ts +307 -0
  40. package/src/__tests/WalletPermissionsManager.flows.test.ts +462 -0
  41. package/src/__tests/WalletPermissionsManager.initialization.test.ts +300 -0
  42. package/src/__tests/WalletPermissionsManager.pmodules.test.ts +798 -0
  43. package/src/__tests/WalletPermissionsManager.proxying.test.ts +724 -0
  44. package/src/__tests/WalletPermissionsManager.tokens.test.ts +503 -0
  45. package/src/index.all.ts +27 -0
  46. package/src/index.client.ts +25 -0
  47. package/src/index.mobile.ts +21 -0
  48. package/src/index.ts +1 -0
  49. package/src/monitor/Monitor.ts +412 -0
  50. package/src/monitor/MonitorDaemon.ts +188 -0
  51. package/src/monitor/README.md +3 -0
  52. package/src/monitor/__test/MonitorDaemon.man.test.ts +45 -0
  53. package/src/monitor/tasks/TaskCheckForProofs.ts +243 -0
  54. package/src/monitor/tasks/TaskCheckNoSends.ts +73 -0
  55. package/src/monitor/tasks/TaskClock.ts +33 -0
  56. package/src/monitor/tasks/TaskFailAbandoned.ts +54 -0
  57. package/src/monitor/tasks/TaskMonitorCallHistory.ts +26 -0
  58. package/src/monitor/tasks/TaskNewHeader.ts +93 -0
  59. package/src/monitor/tasks/TaskPurge.ts +68 -0
  60. package/src/monitor/tasks/TaskReorg.ts +89 -0
  61. package/src/monitor/tasks/TaskReviewStatus.ts +48 -0
  62. package/src/monitor/tasks/TaskSendWaiting.ts +122 -0
  63. package/src/monitor/tasks/TaskSyncWhenIdle.ts +26 -0
  64. package/src/monitor/tasks/TaskUnFail.ts +151 -0
  65. package/src/monitor/tasks/WalletMonitorTask.ts +47 -0
  66. package/src/sdk/CertOpsWallet.ts +18 -0
  67. package/src/sdk/PrivilegedKeyManager.ts +372 -0
  68. package/src/sdk/README.md +13 -0
  69. package/src/sdk/WERR_errors.ts +234 -0
  70. package/src/sdk/WalletError.ts +170 -0
  71. package/src/sdk/WalletErrorFromJson.ts +80 -0
  72. package/src/sdk/WalletServices.interfaces.ts +700 -0
  73. package/src/sdk/WalletSigner.interfaces.ts +11 -0
  74. package/src/sdk/WalletStorage.interfaces.ts +606 -0
  75. package/src/sdk/__test/CertificateLifeCycle.test.ts +131 -0
  76. package/src/sdk/__test/PrivilegedKeyManager.test.ts +738 -0
  77. package/src/sdk/__test/WalletError.test.ts +318 -0
  78. package/src/sdk/__test/validationHelpers.test.ts +21 -0
  79. package/src/sdk/index.ts +10 -0
  80. package/src/sdk/types.ts +226 -0
  81. package/src/services/README.md +11 -0
  82. package/src/services/ServiceCollection.ts +248 -0
  83. package/src/services/Services.ts +603 -0
  84. package/src/services/__tests/ARC.man.test.ts +123 -0
  85. package/src/services/__tests/ARC.timeout.man.test.ts +79 -0
  86. package/src/services/__tests/ArcGorillaPool.man.test.ts +108 -0
  87. package/src/services/__tests/arcServices.test.ts +8 -0
  88. package/src/services/__tests/bitrails.test.ts +56 -0
  89. package/src/services/__tests/getMerklePath.test.ts +15 -0
  90. package/src/services/__tests/getRawTx.test.ts +13 -0
  91. package/src/services/__tests/postBeef.test.ts +104 -0
  92. package/src/services/__tests/verifyBeef.test.ts +50 -0
  93. package/src/services/chaintracker/BHServiceClient.ts +212 -0
  94. package/src/services/chaintracker/ChaintracksChainTracker.ts +71 -0
  95. package/src/services/chaintracker/__tests/ChaintracksChainTracker.test.ts +33 -0
  96. package/src/services/chaintracker/__tests/ChaintracksServiceClient.test.ts +29 -0
  97. package/src/services/chaintracker/chaintracks/Api/BlockHeaderApi.ts +72 -0
  98. package/src/services/chaintracker/chaintracks/Api/BulkIngestorApi.ts +83 -0
  99. package/src/services/chaintracker/chaintracks/Api/BulkStorageApi.ts +92 -0
  100. package/src/services/chaintracker/chaintracks/Api/ChaintracksApi.ts +64 -0
  101. package/src/services/chaintracker/chaintracks/Api/ChaintracksClientApi.ts +189 -0
  102. package/src/services/chaintracker/chaintracks/Api/ChaintracksFetchApi.ts +18 -0
  103. package/src/services/chaintracker/chaintracks/Api/ChaintracksFsApi.ts +58 -0
  104. package/src/services/chaintracker/chaintracks/Api/ChaintracksStorageApi.ts +386 -0
  105. package/src/services/chaintracker/chaintracks/Api/LiveIngestorApi.ts +25 -0
  106. package/src/services/chaintracker/chaintracks/Chaintracks.ts +609 -0
  107. package/src/services/chaintracker/chaintracks/ChaintracksService.ts +199 -0
  108. package/src/services/chaintracker/chaintracks/ChaintracksServiceClient.ts +154 -0
  109. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorBase.ts +176 -0
  110. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorCDN.ts +174 -0
  111. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorCDNBabbage.ts +18 -0
  112. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.ts +113 -0
  113. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainWs.ts +81 -0
  114. package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorBase.ts +86 -0
  115. package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorTeranodeP2P.ts +59 -0
  116. package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainPoll.ts +104 -0
  117. package/src/services/chaintracker/chaintracks/Ingest/LiveIngestorWhatsOnChainWs.ts +66 -0
  118. package/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainIngestorWs.ts +566 -0
  119. package/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainServices.ts +219 -0
  120. package/src/services/chaintracker/chaintracks/Ingest/__tests/BulkIngestorCDNBabbage.test.ts +54 -0
  121. package/src/services/chaintracker/chaintracks/Ingest/__tests/LiveIngestorWhatsOnChainPoll.test.ts +33 -0
  122. package/src/services/chaintracker/chaintracks/Ingest/__tests/WhatsOnChainServices.test.ts +124 -0
  123. package/src/services/chaintracker/chaintracks/Storage/BulkStorageBase.ts +92 -0
  124. package/src/services/chaintracker/chaintracks/Storage/ChaintracksKnexMigrations.ts +104 -0
  125. package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.ts +382 -0
  126. package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageIdb.ts +574 -0
  127. package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.ts +438 -0
  128. package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageMemory.ts +29 -0
  129. package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageNoDb.ts +304 -0
  130. package/src/services/chaintracker/chaintracks/Storage/__tests/ChaintracksStorageIdb.test.ts +102 -0
  131. package/src/services/chaintracker/chaintracks/Storage/__tests/ChaintracksStorageKnex.test.ts +45 -0
  132. package/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.ts +77 -0
  133. package/src/services/chaintracker/chaintracks/__tests/ChaintracksClientApi.test.ts +192 -0
  134. package/src/services/chaintracker/chaintracks/__tests/LocalCdnServer.ts +75 -0
  135. package/src/services/chaintracker/chaintracks/__tests/createIdbChaintracks.test.ts +62 -0
  136. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNetBlockHeaders.json +1 -0
  137. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_0.headers +0 -0
  138. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_1.headers +0 -0
  139. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_2.headers +0 -0
  140. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest349/mainNet_3.headers +0 -0
  141. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNetBlockHeaders.json +1 -0
  142. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_0.headers +0 -0
  143. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_1.headers +0 -0
  144. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_2.headers +0 -0
  145. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest379/mainNet_3.headers +0 -0
  146. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNetBlockHeaders.json +1 -0
  147. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_0.headers +0 -0
  148. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_1.headers +0 -0
  149. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_2.headers +0 -0
  150. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest399/mainNet_3.headers +0 -0
  151. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNetBlockHeaders.json +1 -0
  152. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_0.headers +0 -0
  153. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_1.headers +0 -0
  154. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_2.headers +0 -0
  155. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_3.headers +0 -0
  156. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest402/mainNet_4.headers +0 -0
  157. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNetBlockHeaders.json +1 -0
  158. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_0.headers +0 -0
  159. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_1.headers +0 -0
  160. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_2.headers +0 -0
  161. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_3.headers +0 -0
  162. package/src/services/chaintracker/chaintracks/__tests/data/cdnTest499/mainNet_4.headers +0 -0
  163. package/src/services/chaintracker/chaintracks/createDefaultIdbChaintracksOptions.ts +92 -0
  164. package/src/services/chaintracker/chaintracks/createDefaultKnexChaintracksOptions.ts +111 -0
  165. package/src/services/chaintracker/chaintracks/createDefaultNoDbChaintracksOptions.ts +91 -0
  166. package/src/services/chaintracker/chaintracks/createIdbChaintracks.ts +60 -0
  167. package/src/services/chaintracker/chaintracks/createKnexChaintracks.ts +65 -0
  168. package/src/services/chaintracker/chaintracks/createNoDbChaintracks.ts +60 -0
  169. package/src/services/chaintracker/chaintracks/index.all.ts +12 -0
  170. package/src/services/chaintracker/chaintracks/index.client.ts +4 -0
  171. package/src/services/chaintracker/chaintracks/index.mobile.ts +37 -0
  172. package/src/services/chaintracker/chaintracks/util/BulkFileDataManager.ts +975 -0
  173. package/src/services/chaintracker/chaintracks/util/BulkFileDataReader.ts +60 -0
  174. package/src/services/chaintracker/chaintracks/util/BulkFilesReader.ts +336 -0
  175. package/src/services/chaintracker/chaintracks/util/BulkHeaderFile.ts +247 -0
  176. package/src/services/chaintracker/chaintracks/util/ChaintracksFetch.ts +69 -0
  177. package/src/services/chaintracker/chaintracks/util/ChaintracksFs.ts +141 -0
  178. package/src/services/chaintracker/chaintracks/util/HeightRange.ts +153 -0
  179. package/src/services/chaintracker/chaintracks/util/SingleWriterMultiReaderLock.ts +76 -0
  180. package/src/services/chaintracker/chaintracks/util/__tests/BulkFileDataManager.test.ts +304 -0
  181. package/src/services/chaintracker/chaintracks/util/__tests/ChaintracksFetch.test.ts +60 -0
  182. package/src/services/chaintracker/chaintracks/util/__tests/HeightRange.test.ts +67 -0
  183. package/src/services/chaintracker/chaintracks/util/__tests/SingleWriterMultiReaderLock.test.ts +49 -0
  184. package/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.ts +573 -0
  185. package/src/services/chaintracker/chaintracks/util/dirtyHashes.ts +29 -0
  186. package/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.ts +432 -0
  187. package/src/services/chaintracker/index.all.ts +4 -0
  188. package/src/services/chaintracker/index.client.ts +4 -0
  189. package/src/services/chaintracker/index.mobile.ts +4 -0
  190. package/src/services/createDefaultWalletServicesOptions.ts +77 -0
  191. package/src/services/index.ts +1 -0
  192. package/src/services/processingErrors/arcSuccessError.json +76 -0
  193. package/src/services/providers/ARC.ts +350 -0
  194. package/src/services/providers/Bitails.ts +256 -0
  195. package/src/services/providers/SdkWhatsOnChain.ts +83 -0
  196. package/src/services/providers/WhatsOnChain.ts +883 -0
  197. package/src/services/providers/__tests/WhatsOnChain.test.ts +242 -0
  198. package/src/services/providers/__tests/exchangeRates.test.ts +18 -0
  199. package/src/services/providers/exchangeRates.ts +265 -0
  200. package/src/services/providers/getBeefForTxid.ts +369 -0
  201. package/src/signer/README.md +5 -0
  202. package/src/signer/WalletSigner.ts +17 -0
  203. package/src/signer/methods/acquireDirectCertificate.ts +52 -0
  204. package/src/signer/methods/buildSignableTransaction.ts +183 -0
  205. package/src/signer/methods/completeSignedTransaction.ts +117 -0
  206. package/src/signer/methods/createAction.ts +172 -0
  207. package/src/signer/methods/internalizeAction.ts +106 -0
  208. package/src/signer/methods/proveCertificate.ts +43 -0
  209. package/src/signer/methods/signAction.ts +54 -0
  210. package/src/storage/README.md +14 -0
  211. package/src/storage/StorageIdb.ts +2304 -0
  212. package/src/storage/StorageKnex.ts +1425 -0
  213. package/src/storage/StorageProvider.ts +810 -0
  214. package/src/storage/StorageReader.ts +194 -0
  215. package/src/storage/StorageReaderWriter.ts +432 -0
  216. package/src/storage/StorageSyncReader.ts +34 -0
  217. package/src/storage/WalletStorageManager.ts +943 -0
  218. package/src/storage/__test/StorageIdb.test.ts +43 -0
  219. package/src/storage/__test/WalletStorageManager.test.ts +275 -0
  220. package/src/storage/__test/adminStats.man.test.ts +89 -0
  221. package/src/storage/__test/getBeefForTransaction.test.ts +385 -0
  222. package/src/storage/index.all.ts +11 -0
  223. package/src/storage/index.client.ts +7 -0
  224. package/src/storage/index.mobile.ts +6 -0
  225. package/src/storage/methods/ListActionsSpecOp.ts +70 -0
  226. package/src/storage/methods/ListOutputsSpecOp.ts +129 -0
  227. package/src/storage/methods/__test/GenerateChange/generateChangeSdk.test.ts +1057 -0
  228. package/src/storage/methods/__test/GenerateChange/randomValsUsed1.ts +20 -0
  229. package/src/storage/methods/__test/offsetKey.test.ts +274 -0
  230. package/src/storage/methods/attemptToPostReqsToNetwork.ts +389 -0
  231. package/src/storage/methods/createAction.ts +947 -0
  232. package/src/storage/methods/generateChange.ts +556 -0
  233. package/src/storage/methods/getBeefForTransaction.ts +139 -0
  234. package/src/storage/methods/getSyncChunk.ts +293 -0
  235. package/src/storage/methods/internalizeAction.ts +562 -0
  236. package/src/storage/methods/listActionsIdb.ts +183 -0
  237. package/src/storage/methods/listActionsKnex.ts +226 -0
  238. package/src/storage/methods/listCertificates.ts +73 -0
  239. package/src/storage/methods/listOutputsIdb.ts +203 -0
  240. package/src/storage/methods/listOutputsKnex.ts +263 -0
  241. package/src/storage/methods/offsetKey.ts +89 -0
  242. package/src/storage/methods/processAction.ts +420 -0
  243. package/src/storage/methods/purgeData.ts +251 -0
  244. package/src/storage/methods/purgeDataIdb.ts +10 -0
  245. package/src/storage/methods/reviewStatus.ts +101 -0
  246. package/src/storage/methods/reviewStatusIdb.ts +43 -0
  247. package/src/storage/methods/utils.Buffer.ts +33 -0
  248. package/src/storage/methods/utils.ts +56 -0
  249. package/src/storage/remoting/StorageClient.ts +567 -0
  250. package/src/storage/remoting/StorageMobile.ts +544 -0
  251. package/src/storage/remoting/StorageServer.ts +291 -0
  252. package/src/storage/remoting/__test/StorageClient.test.ts +113 -0
  253. package/src/storage/schema/KnexMigrations.ts +489 -0
  254. package/src/storage/schema/StorageIdbSchema.ts +150 -0
  255. package/src/storage/schema/entities/EntityBase.ts +210 -0
  256. package/src/storage/schema/entities/EntityCertificate.ts +188 -0
  257. package/src/storage/schema/entities/EntityCertificateField.ts +136 -0
  258. package/src/storage/schema/entities/EntityCommission.ts +148 -0
  259. package/src/storage/schema/entities/EntityOutput.ts +290 -0
  260. package/src/storage/schema/entities/EntityOutputBasket.ts +153 -0
  261. package/src/storage/schema/entities/EntityOutputTag.ts +121 -0
  262. package/src/storage/schema/entities/EntityOutputTagMap.ts +123 -0
  263. package/src/storage/schema/entities/EntityProvenTx.ts +319 -0
  264. package/src/storage/schema/entities/EntityProvenTxReq.ts +580 -0
  265. package/src/storage/schema/entities/EntitySyncState.ts +389 -0
  266. package/src/storage/schema/entities/EntityTransaction.ts +306 -0
  267. package/src/storage/schema/entities/EntityTxLabel.ts +121 -0
  268. package/src/storage/schema/entities/EntityTxLabelMap.ts +123 -0
  269. package/src/storage/schema/entities/EntityUser.ts +112 -0
  270. package/src/storage/schema/entities/MergeEntity.ts +73 -0
  271. package/src/storage/schema/entities/__tests/CertificateFieldTests.test.ts +353 -0
  272. package/src/storage/schema/entities/__tests/CertificateTests.test.ts +354 -0
  273. package/src/storage/schema/entities/__tests/CommissionTests.test.ts +371 -0
  274. package/src/storage/schema/entities/__tests/OutputBasketTests.test.ts +278 -0
  275. package/src/storage/schema/entities/__tests/OutputTagMapTests.test.ts +242 -0
  276. package/src/storage/schema/entities/__tests/OutputTagTests.test.ts +288 -0
  277. package/src/storage/schema/entities/__tests/OutputTests.test.ts +464 -0
  278. package/src/storage/schema/entities/__tests/ProvenTxReqTests.test.ts +340 -0
  279. package/src/storage/schema/entities/__tests/ProvenTxTests.test.ts +504 -0
  280. package/src/storage/schema/entities/__tests/SyncStateTests.test.ts +288 -0
  281. package/src/storage/schema/entities/__tests/TransactionTests.test.ts +604 -0
  282. package/src/storage/schema/entities/__tests/TxLabelMapTests.test.ts +361 -0
  283. package/src/storage/schema/entities/__tests/TxLabelTests.test.ts +198 -0
  284. package/src/storage/schema/entities/__tests/stampLogTests.test.ts +90 -0
  285. package/src/storage/schema/entities/__tests/usersTests.test.ts +340 -0
  286. package/src/storage/schema/entities/index.ts +16 -0
  287. package/src/storage/schema/tables/TableCertificate.ts +21 -0
  288. package/src/storage/schema/tables/TableCertificateField.ts +12 -0
  289. package/src/storage/schema/tables/TableCommission.ts +13 -0
  290. package/src/storage/schema/tables/TableMonitorEvent.ts +9 -0
  291. package/src/storage/schema/tables/TableOutput.ts +64 -0
  292. package/src/storage/schema/tables/TableOutputBasket.ts +12 -0
  293. package/src/storage/schema/tables/TableOutputTag.ts +10 -0
  294. package/src/storage/schema/tables/TableOutputTagMap.ts +9 -0
  295. package/src/storage/schema/tables/TableProvenTx.ts +14 -0
  296. package/src/storage/schema/tables/TableProvenTxReq.ts +65 -0
  297. package/src/storage/schema/tables/TableSettings.ts +17 -0
  298. package/src/storage/schema/tables/TableSyncState.ts +18 -0
  299. package/src/storage/schema/tables/TableTransaction.ts +54 -0
  300. package/src/storage/schema/tables/TableTxLabel.ts +10 -0
  301. package/src/storage/schema/tables/TableTxLabelMap.ts +9 -0
  302. package/src/storage/schema/tables/TableUser.ts +16 -0
  303. package/src/storage/schema/tables/index.ts +16 -0
  304. package/src/storage/sync/StorageMySQLDojoReader.ts +696 -0
  305. package/src/storage/sync/index.ts +1 -0
  306. package/src/utility/Format.ts +133 -0
  307. package/src/utility/README.md +3 -0
  308. package/src/utility/ReaderUint8Array.ts +187 -0
  309. package/src/utility/ScriptTemplateBRC29.ts +73 -0
  310. package/src/utility/__tests/utilityHelpers.noBuffer.test.ts +109 -0
  311. package/src/utility/aggregateResults.ts +68 -0
  312. package/src/utility/identityUtils.ts +159 -0
  313. package/src/utility/index.all.ts +7 -0
  314. package/src/utility/index.client.ts +7 -0
  315. package/src/utility/parseTxScriptOffsets.ts +29 -0
  316. package/src/utility/stampLog.ts +69 -0
  317. package/src/utility/tscProofToMerklePath.ts +48 -0
  318. package/src/utility/utilityHelpers.buffer.ts +34 -0
  319. package/src/utility/utilityHelpers.noBuffer.ts +60 -0
  320. package/src/utility/utilityHelpers.ts +275 -0
  321. package/src/wab-client/WABClient.ts +94 -0
  322. package/src/wab-client/__tests/WABClient.man.test.ts +59 -0
  323. package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +47 -0
  324. package/src/wab-client/auth-method-interactors/DevConsoleInteractor.ts +73 -0
  325. package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +35 -0
  326. package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +72 -0
  327. package/syncVersions.js +71 -0
  328. package/test/Wallet/StorageClient/storageClient.man.test.ts +75 -0
  329. package/test/Wallet/action/abortAction.test.ts +47 -0
  330. package/test/Wallet/action/createAction.test.ts +299 -0
  331. package/test/Wallet/action/createAction2.test.ts +1273 -0
  332. package/test/Wallet/action/createActionToGenerateBeefs.man.test.ts +293 -0
  333. package/test/Wallet/action/internalizeAction.a.test.ts +286 -0
  334. package/test/Wallet/action/internalizeAction.test.ts +682 -0
  335. package/test/Wallet/action/relinquishOutput.test.ts +37 -0
  336. package/test/Wallet/certificate/acquireCertificate.test.ts +298 -0
  337. package/test/Wallet/certificate/listCertificates.test.ts +346 -0
  338. package/test/Wallet/construct/Wallet.constructor.test.ts +57 -0
  339. package/test/Wallet/get/getHeaderForHeight.test.ts +82 -0
  340. package/test/Wallet/get/getHeight.test.ts +52 -0
  341. package/test/Wallet/get/getKnownTxids.test.ts +86 -0
  342. package/test/Wallet/get/getNetwork.test.ts +27 -0
  343. package/test/Wallet/get/getVersion.test.ts +27 -0
  344. package/test/Wallet/list/listActions.test.ts +279 -0
  345. package/test/Wallet/list/listActions2.test.ts +1381 -0
  346. package/test/Wallet/list/listCertificates.test.ts +118 -0
  347. package/test/Wallet/list/listOutputs.test.ts +447 -0
  348. package/test/Wallet/live/walletLive.man.test.ts +521 -0
  349. package/test/Wallet/local/localWallet.man.test.ts +93 -0
  350. package/test/Wallet/local/localWallet2.man.test.ts +277 -0
  351. package/test/Wallet/signAction/mountaintop.man.test.ts +130 -0
  352. package/test/Wallet/specOps/specOps.man.test.ts +220 -0
  353. package/test/Wallet/support/janitor.man.test.ts +40 -0
  354. package/test/Wallet/support/operations.man.test.ts +407 -0
  355. package/test/Wallet/support/reqErrorReview.2025.05.06.man.test.ts +347 -0
  356. package/test/Wallet/sync/Wallet.sync.test.ts +215 -0
  357. package/test/Wallet/sync/Wallet.updateWalletLegacyTestData.man.test.ts +203 -0
  358. package/test/Wallet/sync/setActive.test.ts +170 -0
  359. package/test/WalletClient/LocalKVStore.man.test.ts +114 -0
  360. package/test/WalletClient/WERR.man.test.ts +35 -0
  361. package/test/bsv-ts-sdk/LocalKVStore.test.ts +102 -0
  362. package/test/checkDB.ts +57 -0
  363. package/test/checkdb +0 -0
  364. package/test/examples/backup.man.test.ts +59 -0
  365. package/test/examples/pushdrop.test.ts +282 -0
  366. package/test/monitor/Monitor.test.ts +620 -0
  367. package/test/services/Services.test.ts +263 -0
  368. package/test/storage/KnexMigrations.test.ts +86 -0
  369. package/test/storage/StorageMySQLDojoReader.man.test.ts +60 -0
  370. package/test/storage/count.test.ts +177 -0
  371. package/test/storage/find.test.ts +195 -0
  372. package/test/storage/findLegacy.test.ts +67 -0
  373. package/test/storage/idb/allocateChange.test.ts +251 -0
  374. package/test/storage/idb/count.test.ts +158 -0
  375. package/test/storage/idb/find.test.ts +177 -0
  376. package/test/storage/idb/idbSpeed.test.ts +36 -0
  377. package/test/storage/idb/insert.test.ts +268 -0
  378. package/test/storage/idb/transactionAbort.test.ts +108 -0
  379. package/test/storage/idb/update.test.ts +999 -0
  380. package/test/storage/insert.test.ts +278 -0
  381. package/test/storage/update.test.ts +1021 -0
  382. package/test/storage/update2.test.ts +897 -0
  383. package/test/utils/TestUtilsWalletStorage.ts +2526 -0
  384. package/test/utils/localWalletMethods.ts +363 -0
  385. package/test/utils/removeFailedFromDatabase.sql +17 -0
  386. package/ts2md.json +44 -0
  387. package/tsconfig.all.json +31 -0
  388. package/tsconfig.client.json +29 -0
  389. package/tsconfig.json +17 -0
  390. package/tsconfig.mobile.json +28 -0
@@ -0,0 +1,1999 @@
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
+ BigNumber,
66
+ Curve
67
+ } from '@bsv/sdk'
68
+ import { PrivilegedKeyManager } from './sdk/PrivilegedKeyManager'
69
+
70
+ /**
71
+ * Number of rounds used in PBKDF2 for deriving password keys.
72
+ */
73
+ export const PBKDF2_NUM_ROUNDS = 7777
74
+
75
+ /**
76
+ * PBKDF-2 that prefers the browser / Node 20+ WebCrypto implementation and
77
+ * silently falls back to the existing JS code.
78
+ *
79
+ * @param passwordBytes Raw password bytes.
80
+ * @param salt Salt bytes.
81
+ * @param iterations Number of rounds.
82
+ * @param keyLen Desired key length in bytes.
83
+ * @param hash Digest algorithm (default "sha512").
84
+ * @returns Derived key bytes.
85
+ */
86
+ async function pbkdf2NativeOrJs(
87
+ passwordBytes: number[],
88
+ salt: number[],
89
+ iterations: number,
90
+ keyLen: number,
91
+ hash: 'sha256' | 'sha512' = 'sha512'
92
+ ): Promise<number[]> {
93
+ // ----- fast-path: WebCrypto (both browser & recent Node expose globalThis.crypto.subtle)
94
+ const subtle = (globalThis as any)?.crypto?.subtle as SubtleCrypto | undefined
95
+ if (subtle) {
96
+ try {
97
+ const baseKey = await subtle.importKey(
98
+ 'raw',
99
+ new Uint8Array(passwordBytes),
100
+ { name: 'PBKDF2' },
101
+ /*extractable*/ false,
102
+ ['deriveBits']
103
+ )
104
+
105
+ const bits = await subtle.deriveBits(
106
+ {
107
+ name: 'PBKDF2',
108
+ salt: new Uint8Array(salt),
109
+ iterations,
110
+ hash: hash.toUpperCase() as AlgorithmIdentifier
111
+ },
112
+ baseKey,
113
+ keyLen * 8
114
+ )
115
+ return Array.from(new Uint8Array(bits))
116
+ } catch (err) {
117
+ //console.warn('[pbkdf2] WebCrypto path failed → falling back to JS implementation', err)
118
+ /* fall through */
119
+ }
120
+ }
121
+
122
+ // ----- slow-path: old JavaScript implementation
123
+ return Hash.pbkdf2(passwordBytes, salt, iterations, keyLen, hash)
124
+ }
125
+
126
+ /**
127
+ * Unique Identifier for the default profile (16 zero bytes).
128
+ */
129
+ export const DEFAULT_PROFILE_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
130
+
131
+ /**
132
+ * Describes the structure of a user profile within the wallet.
133
+ */
134
+ export interface Profile {
135
+ /**
136
+ * User-defined name for the profile.
137
+ */
138
+ name: string
139
+
140
+ /**
141
+ * Unique 16-byte identifier for the profile.
142
+ */
143
+ id: number[]
144
+
145
+ /**
146
+ * 32-byte random pad XOR'd with the root primary key to derive the profile's primary key.
147
+ */
148
+ primaryPad: number[]
149
+
150
+ /**
151
+ * 32-byte random pad XOR'd with the root privileged key to derive the profile's privileged key.
152
+ */
153
+ privilegedPad: number[]
154
+
155
+ /**
156
+ * Timestamp (seconds since epoch) when the profile was created.
157
+ */
158
+ createdAt: number
159
+ }
160
+
161
+ /**
162
+ * Describes the structure of a User Management Protocol (UMP) token.
163
+ */
164
+ export interface UMPToken {
165
+ /**
166
+ * Root Primary key encrypted by the XOR of the password and presentation keys.
167
+ */
168
+ passwordPresentationPrimary: number[]
169
+
170
+ /**
171
+ * Root Primary key encrypted by the XOR of the password and recovery keys.
172
+ */
173
+ passwordRecoveryPrimary: number[]
174
+
175
+ /**
176
+ * Root Primary key encrypted by the XOR of the presentation and recovery keys.
177
+ */
178
+ presentationRecoveryPrimary: number[]
179
+
180
+ /**
181
+ * Root Privileged key encrypted by the XOR of the password and primary keys.
182
+ */
183
+ passwordPrimaryPrivileged: number[]
184
+
185
+ /**
186
+ * Root Privileged key encrypted by the XOR of the presentation and recovery keys.
187
+ */
188
+ presentationRecoveryPrivileged: number[]
189
+
190
+ /**
191
+ * Hash of the presentation key.
192
+ */
193
+ presentationHash: number[]
194
+
195
+ /**
196
+ * PBKDF2 salt used in conjunction with the password to derive the password key.
197
+ */
198
+ passwordSalt: number[]
199
+
200
+ /**
201
+ * Hash of the recovery key.
202
+ */
203
+ recoveryHash: number[]
204
+
205
+ /**
206
+ * A copy of the presentation key encrypted with the root privileged key.
207
+ */
208
+ presentationKeyEncrypted: number[]
209
+
210
+ /**
211
+ * A copy of the recovery key encrypted with the root privileged key.
212
+ */
213
+ recoveryKeyEncrypted: number[]
214
+
215
+ /**
216
+ * A copy of the password key encrypted with the root privileged key.
217
+ */
218
+ passwordKeyEncrypted: number[]
219
+
220
+ /**
221
+ * Optional field containing the encrypted profile data.
222
+ * JSON string -> Encrypted Bytes using root privileged key.
223
+ */
224
+ profilesEncrypted?: number[]
225
+
226
+ /**
227
+ * Describes the token's location on-chain, if it's already been published.
228
+ */
229
+ currentOutpoint?: OutpointString
230
+ }
231
+
232
+ /**
233
+ * Describes a system capable of finding and updating UMP tokens on the blockchain.
234
+ */
235
+ export interface UMPTokenInteractor {
236
+ /**
237
+ * Locates the latest valid copy of a UMP token (including its outpoint)
238
+ * based on the presentation key hash.
239
+ *
240
+ * @param hash The hash of the presentation key.
241
+ * @returns The UMP token if found; otherwise, undefined.
242
+ */
243
+ findByPresentationKeyHash: (hash: number[]) => Promise<UMPToken | undefined>
244
+
245
+ /**
246
+ * Locates the latest valid copy of a UMP token (including its outpoint)
247
+ * based on the recovery key hash.
248
+ *
249
+ * @param hash The hash of the recovery key.
250
+ * @returns The UMP token if found; otherwise, undefined.
251
+ */
252
+ findByRecoveryKeyHash: (hash: number[]) => Promise<UMPToken | undefined>
253
+
254
+ /**
255
+ * Creates (and optionally consumes the previous version of) a UMP token on-chain.
256
+ *
257
+ * @param wallet The wallet that might be used to create a new token (MUST be operating under the DEFAULT profile).
258
+ * @param adminOriginator The domain name of the administrative originator.
259
+ * @param token The new UMP token to create.
260
+ * @param oldTokenToConsume If provided, the old token that must be consumed in the same transaction.
261
+ * @returns The newly created outpoint.
262
+ */
263
+ buildAndSend: (
264
+ wallet: WalletInterface, // This wallet MUST be the one built for the default profile
265
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes,
266
+ token: UMPToken,
267
+ oldTokenToConsume?: UMPToken
268
+ ) => Promise<OutpointString>
269
+ }
270
+
271
+ /**
272
+ * @class OverlayUMPTokenInteractor
273
+ *
274
+ * A concrete implementation of the UMPTokenInteractor interface that interacts
275
+ * with Overlay Services and the UMP (User Management Protocol) topic. This class
276
+ * is responsible for:
277
+ *
278
+ * 1) Locating UMP tokens via overlay lookups (ls_users).
279
+ * 2) Creating and publishing new or updated UMP token outputs on-chain under
280
+ * the "tm_users" topic.
281
+ * 3) Consuming (spending) an old token if provided.
282
+ */
283
+ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
284
+ /**
285
+ * A `LookupResolver` instance used to query overlay networks.
286
+ */
287
+ private readonly resolver: LookupResolver
288
+
289
+ /**
290
+ * A SHIP broadcaster that can be used to publish updated UMP tokens
291
+ * under the `tm_users` topic to overlay service peers.
292
+ */
293
+ private readonly broadcaster: SHIPBroadcaster
294
+
295
+ /**
296
+ * Construct a new OverlayUMPTokenInteractor.
297
+ *
298
+ * @param resolver A LookupResolver instance for performing overlay queries (ls_users).
299
+ * @param broadcaster A SHIPBroadcaster instance for sharing new or updated tokens across the `tm_users` overlay.
300
+ */
301
+ constructor(
302
+ resolver: LookupResolver = new LookupResolver(),
303
+ broadcaster: SHIPBroadcaster = new SHIPBroadcaster(['tm_users'])
304
+ ) {
305
+ this.resolver = resolver
306
+ this.broadcaster = broadcaster
307
+ }
308
+
309
+ /**
310
+ * Finds a UMP token on-chain by the given presentation key hash, if it exists.
311
+ * Uses the ls_users overlay service to perform the lookup.
312
+ *
313
+ * @param hash The 32-byte SHA-256 hash of the presentation key.
314
+ * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
315
+ */
316
+ public async findByPresentationKeyHash(hash: number[]): Promise<UMPToken | undefined> {
317
+ // Query ls_users for the given presentationHash
318
+ const question = {
319
+ service: 'ls_users',
320
+ query: { presentationHash: Utils.toHex(hash) }
321
+ }
322
+ const answer = await this.resolver.query(question)
323
+ return this.parseLookupAnswer(answer)
324
+ }
325
+
326
+ /**
327
+ * Finds a UMP token on-chain by the given recovery key hash, if it exists.
328
+ * Uses the ls_users overlay service to perform the lookup.
329
+ *
330
+ * @param hash The 32-byte SHA-256 hash of the recovery key.
331
+ * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
332
+ */
333
+ public async findByRecoveryKeyHash(hash: number[]): Promise<UMPToken | undefined> {
334
+ const question = {
335
+ service: 'ls_users',
336
+ query: { recoveryHash: Utils.toHex(hash) }
337
+ }
338
+ const answer = await this.resolver.query(question)
339
+ return this.parseLookupAnswer(answer)
340
+ }
341
+
342
+ /**
343
+ * Creates or updates (replaces) a UMP token on-chain. If `oldTokenToConsume` is provided,
344
+ * it is spent in the same transaction that creates the new token output. The new token is
345
+ * then broadcast and published under the `tm_users` topic using a SHIP broadcast, ensuring
346
+ * overlay participants see the updated token.
347
+ *
348
+ * @param wallet The wallet used to build and sign the transaction (MUST be operating under the DEFAULT profile).
349
+ * @param adminOriginator The domain/FQDN of the administrative originator (wallet operator).
350
+ * @param token The new UMPToken to create on-chain.
351
+ * @param oldTokenToConsume Optionally, an existing token to consume/spend in the same transaction.
352
+ * @returns The outpoint of the newly created UMP token (e.g. "abcd1234...ef.0").
353
+ */
354
+ public async buildAndSend(
355
+ wallet: WalletInterface, // This wallet MUST be the one built for the default profile
356
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes,
357
+ token: UMPToken,
358
+ oldTokenToConsume?: UMPToken
359
+ ): Promise<OutpointString> {
360
+ // 1) Construct the data fields for the new UMP token.
361
+ const fields: number[][] = []
362
+
363
+ fields[0] = token.passwordSalt
364
+ fields[1] = token.passwordPresentationPrimary
365
+ fields[2] = token.passwordRecoveryPrimary
366
+ fields[3] = token.presentationRecoveryPrimary
367
+ fields[4] = token.passwordPrimaryPrivileged
368
+ fields[5] = token.presentationRecoveryPrivileged
369
+ fields[6] = token.presentationHash
370
+ fields[7] = token.recoveryHash
371
+ fields[8] = token.presentationKeyEncrypted
372
+ fields[9] = token.passwordKeyEncrypted
373
+ fields[10] = token.recoveryKeyEncrypted
374
+
375
+ // Optional field (11) for encrypted profiles
376
+ if (token.profilesEncrypted) {
377
+ fields[11] = token.profilesEncrypted
378
+ }
379
+
380
+ // 2) Create a PushDrop script referencing these fields, locked with the admin key.
381
+ const script = await new PushDrop(wallet, adminOriginator).lock(
382
+ fields,
383
+ [2, 'admin user management token'], // protocolID
384
+ '1', // keyID
385
+ 'self', // counterparty
386
+ /*forSelf=*/ true,
387
+ /*includeSignature=*/ true
388
+ )
389
+
390
+ // 3) Prepare the createAction call. If oldTokenToConsume is provided, gather the outpoint.
391
+ const inputs: CreateActionInput[] = []
392
+ let inputToken: { beef: number[]; outputIndex: number } | undefined
393
+ if (oldTokenToConsume?.currentOutpoint) {
394
+ inputToken = await this.findByOutpoint(oldTokenToConsume.currentOutpoint)
395
+ // If there is no token on the overlay, we can't consume it. Just start over with a new token.
396
+ if (!inputToken) {
397
+ oldTokenToConsume = undefined
398
+
399
+ // Otherwise, add the input
400
+ } else {
401
+ inputs.push({
402
+ outpoint: oldTokenToConsume.currentOutpoint,
403
+ unlockingScriptLength: 73, // typical signature length
404
+ inputDescription: 'Consume old UMP token'
405
+ })
406
+ }
407
+ }
408
+
409
+ const outputs = [
410
+ {
411
+ lockingScript: script.toHex(),
412
+ satoshis: 1,
413
+ outputDescription: 'New UMP token output'
414
+ }
415
+ ]
416
+
417
+ // 4) Build the partial transaction via createAction.
418
+ let createResult
419
+ try {
420
+ createResult = await wallet.createAction(
421
+ {
422
+ description: oldTokenToConsume ? 'Renew UMP token (consume old, create new)' : 'Create new UMP token',
423
+ inputs,
424
+ outputs,
425
+ inputBEEF: inputToken?.beef,
426
+ options: {
427
+ randomizeOutputs: false,
428
+ acceptDelayedBroadcast: false
429
+ }
430
+ },
431
+ adminOriginator
432
+ )
433
+ } catch (e) {
434
+ console.error('Error with UMP token update. Attempting a last-ditch effort to get a new one', e)
435
+ createResult = await wallet.createAction(
436
+ {
437
+ description: 'Recover UMP token',
438
+ outputs,
439
+ options: {
440
+ randomizeOutputs: false,
441
+ acceptDelayedBroadcast: false
442
+ }
443
+ },
444
+ adminOriginator
445
+ )
446
+ }
447
+
448
+ // If the transaction is fully processed by the wallet
449
+ if (!createResult.signableTransaction) {
450
+ const finalTxid =
451
+ createResult.txid || (createResult.tx ? Transaction.fromAtomicBEEF(createResult.tx).id('hex') : undefined)
452
+ if (!finalTxid) {
453
+ throw new Error('No signableTransaction and no final TX found.')
454
+ }
455
+ // Now broadcast to `tm_users` using SHIP
456
+ const broadcastTx = Transaction.fromAtomicBEEF(createResult.tx!)
457
+ const result = await this.broadcaster.broadcast(broadcastTx)
458
+ console.log('BROADCAST RESULT', result)
459
+ return `${finalTxid}.0`
460
+ }
461
+
462
+ // 5) If oldTokenToConsume is present, we must sign the input referencing it.
463
+ // (If there's no old token, there's nothing to sign for the input.)
464
+ let finalTxid = ''
465
+ const reference = createResult.signableTransaction.reference
466
+ const partialTx = Transaction.fromBEEF(createResult.signableTransaction.tx)
467
+
468
+ if (oldTokenToConsume?.currentOutpoint) {
469
+ // Unlock the old token with a matching PushDrop unlocker
470
+ const unlocker = new PushDrop(wallet, adminOriginator).unlock([2, 'admin user management token'], '1', 'self')
471
+ const unlockingScript = await unlocker.sign(partialTx, 0)
472
+
473
+ // Provide it to the wallet
474
+ const signResult = await wallet.signAction(
475
+ {
476
+ reference,
477
+ spends: {
478
+ 0: {
479
+ unlockingScript: unlockingScript.toHex()
480
+ }
481
+ }
482
+ },
483
+ adminOriginator
484
+ )
485
+ finalTxid = signResult.txid || (signResult.tx ? Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '')
486
+ if (!finalTxid) {
487
+ throw new Error('Could not finalize transaction for renewed UMP token.')
488
+ }
489
+ // 6) Broadcast to `tm_users`
490
+ const finalAtomicTx = signResult.tx
491
+ if (!finalAtomicTx) {
492
+ throw new Error('Final transaction data missing after signing renewed UMP token.')
493
+ }
494
+ const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx)
495
+ const result = await this.broadcaster.broadcast(broadcastTx)
496
+ console.log('BROADCAST RESULT', result)
497
+ return `${finalTxid}.0`
498
+ } else {
499
+ // Fallback for creating a new token (no input spending)
500
+ const signResult = await wallet.signAction({ reference, spends: {} }, adminOriginator)
501
+ finalTxid = signResult.txid || (signResult.tx ? Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '')
502
+ if (!finalTxid) {
503
+ throw new Error('Failed to finalize new UMP token transaction.')
504
+ }
505
+ const finalAtomicTx = signResult.tx
506
+ if (!finalAtomicTx) {
507
+ throw new Error('Final transaction data missing after signing new UMP token.')
508
+ }
509
+ const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx)
510
+ const result = await this.broadcaster.broadcast(broadcastTx)
511
+ console.log('BROADCAST RESULT', result)
512
+ return `${finalTxid}.0`
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Attempts to parse a LookupAnswer from the UMP lookup service. If successful,
518
+ * extracts the token fields from the resulting transaction and constructs
519
+ * a UMPToken object.
520
+ *
521
+ * @param answer The LookupAnswer returned by a query to ls_users.
522
+ * @returns The parsed UMPToken or `undefined` if none found/decodable.
523
+ */
524
+ private parseLookupAnswer(answer: LookupAnswer): UMPToken | undefined {
525
+ if (answer.type !== 'output-list') {
526
+ return undefined
527
+ }
528
+ if (!answer.outputs || answer.outputs.length === 0) {
529
+ return undefined
530
+ }
531
+
532
+ const { beef, outputIndex } = answer.outputs[0]
533
+ try {
534
+ const tx = Transaction.fromBEEF(beef)
535
+ const outpoint = `${tx.id('hex')}.${outputIndex}`
536
+
537
+ const decoded = PushDrop.decode(tx.outputs[outputIndex].lockingScript)
538
+
539
+ // Expecting 11 or more fields for UMP
540
+ if (!decoded.fields || decoded.fields.length < 11) {
541
+ console.warn(`Unexpected number of fields in UMP token: ${decoded.fields?.length}`)
542
+ return undefined
543
+ }
544
+
545
+ // Build the UMP token from these fields, preserving outpoint
546
+ const t: UMPToken = {
547
+ // Order matches buildAndSend and serialize/deserialize
548
+ passwordSalt: decoded.fields[0],
549
+ passwordPresentationPrimary: decoded.fields[1],
550
+ passwordRecoveryPrimary: decoded.fields[2],
551
+ presentationRecoveryPrimary: decoded.fields[3],
552
+ passwordPrimaryPrivileged: decoded.fields[4],
553
+ presentationRecoveryPrivileged: decoded.fields[5],
554
+ presentationHash: decoded.fields[6],
555
+ recoveryHash: decoded.fields[7],
556
+ presentationKeyEncrypted: decoded.fields[8],
557
+ passwordKeyEncrypted: decoded.fields[9],
558
+ recoveryKeyEncrypted: decoded.fields[10],
559
+ profilesEncrypted: decoded.fields[12] ? decoded.fields[11] : undefined, // If there's a signature in field 12, use field 11
560
+ currentOutpoint: outpoint
561
+ }
562
+ return t
563
+ } catch (e) {
564
+ console.error('Failed to parse or decode UMP token:', e)
565
+ return undefined
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Finds by outpoint for unlocking / spending previous tokens.
571
+ * @param outpoint The outpoint we are searching by
572
+ * @returns The result so that we can use it to unlock the transaction
573
+ */
574
+ private async findByOutpoint(outpoint: string): Promise<{ beef: number[]; outputIndex: number } | undefined> {
575
+ const results = await this.resolver.query({
576
+ service: 'ls_users',
577
+ query: {
578
+ outpoint
579
+ }
580
+ })
581
+ if (results.type !== 'output-list') {
582
+ return undefined
583
+ }
584
+ if (!results.outputs || !results.outputs.length) {
585
+ return undefined
586
+ }
587
+ return results.outputs[0]
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Manages a "CWI-style" wallet that uses a UMP token and a
593
+ * multi-key authentication scheme (password, presentation key, and recovery key),
594
+ * supporting multiple user profiles under a single account.
595
+ */
596
+ export class CWIStyleWalletManager implements WalletInterface {
597
+ /**
598
+ * Whether the user is currently authenticated (i.e., root keys are available).
599
+ */
600
+ authenticated: boolean
601
+
602
+ /**
603
+ * The domain name of the administrative originator (wallet operator / vendor, or your own).
604
+ */
605
+ private adminOriginator: OriginatorDomainNameStringUnder250Bytes
606
+
607
+ /**
608
+ * The system that locates and publishes UMP tokens on-chain.
609
+ */
610
+ private UMPTokenInteractor: UMPTokenInteractor
611
+
612
+ /**
613
+ * A function called to persist the newly generated recovery key.
614
+ * It should generally trigger a UI prompt where the user is asked to write it down.
615
+ */
616
+ private recoveryKeySaver: (key: number[]) => Promise<true>
617
+
618
+ /**
619
+ * Asks the user to enter their password, for a given reason.
620
+ * The test function can be used to see if the password is correct before resolving.
621
+ * Only resolve with the correct password or reject with an error.
622
+ * Resolving with an incorrect password will throw an error.
623
+ */
624
+ private passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>
625
+
626
+ /**
627
+ * Optional function to fund a new Wallet after the new-user flow.
628
+ */
629
+ private newWalletFunder?: (
630
+ presentationKey: number[],
631
+ wallet: WalletInterface, // The default profile wallet
632
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes
633
+ ) => Promise<void>
634
+
635
+ /**
636
+ * Builds the underlying wallet for a specific profile.
637
+ */
638
+ private walletBuilder: (
639
+ profilePrimaryKey: number[],
640
+ profilePrivilegedKeyManager: PrivilegedKeyManager,
641
+ profileId: number[]
642
+ ) => Promise<WalletInterface>
643
+
644
+ /**
645
+ * Current mode of authentication.
646
+ */
647
+ authenticationMode:
648
+ | 'presentation-key-and-password'
649
+ | 'presentation-key-and-recovery-key'
650
+ | 'recovery-key-and-password' = 'presentation-key-and-password'
651
+
652
+ /**
653
+ * Indicates new user or existing user flow.
654
+ */
655
+ authenticationFlow: 'new-user' | 'existing-user' = 'new-user'
656
+
657
+ /**
658
+ * The current UMP token in use.
659
+ */
660
+ private currentUMPToken?: UMPToken
661
+
662
+ /**
663
+ * Temporarily retained presentation key.
664
+ */
665
+ private presentationKey?: number[]
666
+
667
+ /**
668
+ * Temporarily retained recovery key.
669
+ */
670
+ private recoveryKey?: number[]
671
+
672
+ /**
673
+ * The user's *root* primary key, derived from authentication factors.
674
+ */
675
+ private rootPrimaryKey?: number[]
676
+
677
+ /**
678
+ * The currently active profile ID (null or DEFAULT_PROFILE_ID means default profile).
679
+ */
680
+ private activeProfileId: number[] = DEFAULT_PROFILE_ID
681
+
682
+ /**
683
+ * List of loaded non-default profiles.
684
+ */
685
+ private profiles: Profile[] = []
686
+
687
+ /**
688
+ * The underlying wallet instance for the *active* profile.
689
+ */
690
+ private underlying?: WalletInterface
691
+
692
+ /**
693
+ * Privileged key manager associated with the *root* keys, aware of the active profile.
694
+ */
695
+ private rootPrivilegedKeyManager?: PrivilegedKeyManager
696
+
697
+ /**
698
+ * Constructs a new CWIStyleWalletManager.
699
+ *
700
+ * @param adminOriginator The domain name of the administrative originator.
701
+ * @param walletBuilder A function that can build an underlying wallet instance for a profile.
702
+ * @param interactor An instance of UMPTokenInteractor.
703
+ * @param recoveryKeySaver A function to persist a new recovery key.
704
+ * @param passwordRetriever A function to request the user's password.
705
+ * @param newWalletFunder Optional function to fund a new wallet.
706
+ * @param stateSnapshot Optional previously saved state snapshot.
707
+ */
708
+ constructor(
709
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes,
710
+ walletBuilder: (
711
+ profilePrimaryKey: number[],
712
+ profilePrivilegedKeyManager: PrivilegedKeyManager,
713
+ profileId: number[]
714
+ ) => Promise<WalletInterface>,
715
+ interactor: UMPTokenInteractor = new OverlayUMPTokenInteractor(),
716
+ recoveryKeySaver: (key: number[]) => Promise<true>,
717
+ passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>,
718
+ newWalletFunder?: (
719
+ presentationKey: number[],
720
+ wallet: WalletInterface, // Default profile wallet
721
+ adminOriginator: OriginatorDomainNameStringUnder250Bytes
722
+ ) => Promise<void>,
723
+ stateSnapshot?: number[]
724
+ ) {
725
+ this.adminOriginator = adminOriginator
726
+ this.walletBuilder = walletBuilder
727
+ this.UMPTokenInteractor = interactor
728
+ this.recoveryKeySaver = recoveryKeySaver
729
+ this.passwordRetriever = passwordRetriever
730
+ this.authenticated = false
731
+ this.newWalletFunder = newWalletFunder
732
+
733
+ // If a saved snapshot is provided, attempt to load it.
734
+ // Note: loadSnapshot now returns a promise. We don't await it here,
735
+ // as the constructor must be synchronous. The caller should check
736
+ // `this.authenticated` after construction if a snapshot was provided.
737
+ if (stateSnapshot) {
738
+ this.loadSnapshot(stateSnapshot).catch(err => {
739
+ console.error('Failed to load snapshot during construction:', err)
740
+ // Clear potentially partially loaded state
741
+ this.destroy()
742
+ })
743
+ }
744
+ }
745
+
746
+ // --- Authentication Methods ---
747
+
748
+ /**
749
+ * Provides the presentation key.
750
+ */
751
+ async providePresentationKey(key: number[]): Promise<void> {
752
+ if (this.authenticated) {
753
+ throw new Error('User is already authenticated')
754
+ }
755
+ if (this.authenticationMode === 'recovery-key-and-password') {
756
+ throw new Error('Presentation key is not needed in this mode')
757
+ }
758
+
759
+ const hash = Hash.sha256(key)
760
+ const token = await this.UMPTokenInteractor.findByPresentationKeyHash(hash)
761
+
762
+ if (!token) {
763
+ // No token found -> New user
764
+ this.authenticationFlow = 'new-user'
765
+ this.presentationKey = key
766
+ } else {
767
+ // Found token -> existing user
768
+ this.authenticationFlow = 'existing-user'
769
+ this.presentationKey = key
770
+ this.currentUMPToken = token
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Provides the password.
776
+ */
777
+ async providePassword(password: string): Promise<void> {
778
+ if (this.authenticated) {
779
+ throw new Error('User is already authenticated')
780
+ }
781
+ if (this.authenticationMode === 'presentation-key-and-recovery-key') {
782
+ throw new Error('Password is not needed in this mode')
783
+ }
784
+
785
+ if (this.authenticationFlow === 'existing-user') {
786
+ // Existing user flow
787
+ if (!this.currentUMPToken) {
788
+ throw new Error('Provide presentation or recovery key first.')
789
+ }
790
+ const derivedPasswordKey = await pbkdf2NativeOrJs(
791
+ Utils.toArray(password, 'utf8'),
792
+ this.currentUMPToken.passwordSalt,
793
+ PBKDF2_NUM_ROUNDS,
794
+ 32,
795
+ 'sha512'
796
+ )
797
+
798
+ let rootPrimaryKey: number[]
799
+ let rootPrivilegedKey: number[] | undefined // Only needed for recovery mode
800
+
801
+ if (this.authenticationMode === 'presentation-key-and-password') {
802
+ if (!this.presentationKey) throw new Error('No presentation key found!')
803
+ const xorKey = this.XOR(this.presentationKey, derivedPasswordKey)
804
+ rootPrimaryKey = new SymmetricKey(xorKey).decrypt(this.currentUMPToken.passwordPresentationPrimary) as number[]
805
+ } else {
806
+ // 'recovery-key-and-password'
807
+ if (!this.recoveryKey) throw new Error('No recovery key found!')
808
+ const primaryDecryptionKey = this.XOR(this.recoveryKey, derivedPasswordKey)
809
+ rootPrimaryKey = new SymmetricKey(primaryDecryptionKey).decrypt(
810
+ this.currentUMPToken.passwordRecoveryPrimary
811
+ ) as number[]
812
+ const privilegedDecryptionKey = this.XOR(rootPrimaryKey, derivedPasswordKey)
813
+ rootPrivilegedKey = new SymmetricKey(privilegedDecryptionKey).decrypt(
814
+ this.currentUMPToken.passwordPrimaryPrivileged
815
+ ) as number[]
816
+ }
817
+ // Build root infrastructure, load profiles, and switch to default profile initially
818
+ await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey)
819
+ await this.switchProfile(this.activeProfileId)
820
+ } else {
821
+ // New user flow (only 'presentation-key-and-password')
822
+ if (this.authenticationMode !== 'presentation-key-and-password') {
823
+ throw new Error('New-user flow requires presentation key and password mode.')
824
+ }
825
+ if (!this.presentationKey) {
826
+ throw new Error('No presentation key provided for new-user flow.')
827
+ }
828
+
829
+ // Generate new keys/salt
830
+ const recoveryKey = Random(32)
831
+ await this.recoveryKeySaver(recoveryKey)
832
+ const passwordSalt = Random(32)
833
+ const passwordKey = await pbkdf2NativeOrJs(
834
+ Utils.toArray(password, 'utf8'),
835
+ passwordSalt,
836
+ PBKDF2_NUM_ROUNDS,
837
+ 32,
838
+ 'sha512'
839
+ )
840
+ const rootPrimaryKey = Random(32)
841
+ const rootPrivilegedKey = Random(32)
842
+
843
+ // Build XOR keys
844
+ const presentationPassword = new SymmetricKey(this.XOR(this.presentationKey, passwordKey))
845
+ const presentationRecovery = new SymmetricKey(this.XOR(this.presentationKey, recoveryKey))
846
+ const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
847
+ const primaryPassword = new SymmetricKey(this.XOR(rootPrimaryKey, passwordKey))
848
+
849
+ // Temp manager for encryption
850
+ const tempPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(rootPrivilegedKey))
851
+
852
+ // Build new UMP token (no profiles initially)
853
+ const newToken: UMPToken = {
854
+ passwordSalt,
855
+ passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey) as number[],
856
+ passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey) as number[],
857
+ presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey) as number[],
858
+ passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey) as number[],
859
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey) as number[],
860
+ presentationHash: Hash.sha256(this.presentationKey),
861
+ recoveryHash: Hash.sha256(recoveryKey),
862
+ presentationKeyEncrypted: (
863
+ await tempPrivilegedKeyManager.encrypt({
864
+ plaintext: this.presentationKey,
865
+ protocolID: [2, 'admin key wrapping'],
866
+ keyID: '1'
867
+ })
868
+ ).ciphertext,
869
+ passwordKeyEncrypted: (
870
+ await tempPrivilegedKeyManager.encrypt({
871
+ plaintext: passwordKey,
872
+ protocolID: [2, 'admin key wrapping'],
873
+ keyID: '1'
874
+ })
875
+ ).ciphertext,
876
+ recoveryKeyEncrypted: (
877
+ await tempPrivilegedKeyManager.encrypt({
878
+ plaintext: recoveryKey,
879
+ protocolID: [2, 'admin key wrapping'],
880
+ keyID: '1'
881
+ })
882
+ ).ciphertext,
883
+ profilesEncrypted: undefined // No profiles yet
884
+ }
885
+ this.currentUMPToken = newToken
886
+
887
+ // Setup root infrastructure and switch to default profile
888
+ await this.setupRootInfrastructure(rootPrimaryKey)
889
+ await this.switchProfile(DEFAULT_PROFILE_ID)
890
+
891
+ // Fund the *default* wallet if funder provided
892
+ if (this.newWalletFunder && this.underlying) {
893
+ try {
894
+ await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator)
895
+ } catch (e) {
896
+ console.error('Error funding new wallet:', e)
897
+ // Decide if this should halt the process or just log
898
+ }
899
+ }
900
+
901
+ // Publish the new UMP token *after* potentially funding
902
+ // We need the default profile wallet to sign the UMP creation TX
903
+ if (!this.underlying) {
904
+ throw new Error('Default profile wallet not built before attempting to publish UMP token.')
905
+ }
906
+ this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
907
+ this.underlying, // Use the default profile wallet
908
+ this.adminOriginator,
909
+ newToken
910
+ )
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Provides the recovery key.
916
+ */
917
+ async provideRecoveryKey(recoveryKey: number[]): Promise<void> {
918
+ if (this.authenticated) {
919
+ throw new Error('Already authenticated')
920
+ }
921
+ if (this.authenticationFlow === 'new-user') {
922
+ throw new Error('Do not submit recovery key in new-user flow')
923
+ }
924
+
925
+ if (this.authenticationMode === 'presentation-key-and-password') {
926
+ throw new Error('No recovery key required in this mode')
927
+ } else if (this.authenticationMode === 'recovery-key-and-password') {
928
+ // Wait for password
929
+ const hash = Hash.sha256(recoveryKey)
930
+ const token = await this.UMPTokenInteractor.findByRecoveryKeyHash(hash)
931
+ if (!token) throw new Error('No user found with this recovery key')
932
+ this.recoveryKey = recoveryKey
933
+ this.currentUMPToken = token
934
+ } else {
935
+ // 'presentation-key-and-recovery-key'
936
+ if (!this.presentationKey) throw new Error('Provide the presentation key first')
937
+ if (!this.currentUMPToken) throw new Error('Current UMP token not found')
938
+
939
+ const xorKey = this.XOR(this.presentationKey, recoveryKey)
940
+ const rootPrimaryKey = new SymmetricKey(xorKey).decrypt(
941
+ this.currentUMPToken.presentationRecoveryPrimary
942
+ ) as number[]
943
+ const rootPrivilegedKey = new SymmetricKey(xorKey).decrypt(
944
+ this.currentUMPToken.presentationRecoveryPrivileged
945
+ ) as number[]
946
+
947
+ // Build root infrastructure, load profiles, switch to default
948
+ await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey)
949
+ await this.switchProfile(this.activeProfileId)
950
+ }
951
+ }
952
+
953
+ // --- State Management Methods ---
954
+
955
+ /**
956
+ * Saves the current wallet state (root key, UMP token, active profile) into an encrypted snapshot.
957
+ * Version 2 format: [1 byte version=2] + [32 byte snapshot key] + [16 byte activeProfileId] + [encrypted payload]
958
+ * Encrypted Payload: [32 byte rootPrimaryKey] + [varint token length + serialized UMP token]
959
+ *
960
+ * @returns Encrypted snapshot bytes.
961
+ */
962
+ saveSnapshot(): number[] {
963
+ if (!this.rootPrimaryKey || !this.currentUMPToken) {
964
+ throw new Error('No root primary key or current UMP token set')
965
+ }
966
+
967
+ const snapshotKey = Random(32)
968
+ const snapshotPreimageWriter = new Utils.Writer()
969
+
970
+ // Write root primary key
971
+ snapshotPreimageWriter.write(this.rootPrimaryKey)
972
+
973
+ // Write serialized UMP token (must have outpoint)
974
+ if (!this.currentUMPToken.currentOutpoint) {
975
+ throw new Error('UMP token cannot be saved without a current outpoint.')
976
+ }
977
+ const serializedToken = this.serializeUMPToken(this.currentUMPToken)
978
+ snapshotPreimageWriter.writeVarIntNum(serializedToken.length)
979
+ snapshotPreimageWriter.write(serializedToken)
980
+
981
+ // Encrypt the payload
982
+ const snapshotPreimage = snapshotPreimageWriter.toArray()
983
+ const snapshotPayload = new SymmetricKey(snapshotKey).encrypt(snapshotPreimage) as number[]
984
+
985
+ // Build final snapshot (Version 2)
986
+ const snapshotWriter = new Utils.Writer()
987
+ snapshotWriter.writeUInt8(2) // Version
988
+ snapshotWriter.write(snapshotKey)
989
+ snapshotWriter.write(this.activeProfileId) // Active profile ID
990
+ snapshotWriter.write(snapshotPayload) // Encrypted data
991
+
992
+ return snapshotWriter.toArray()
993
+ }
994
+
995
+ /**
996
+ * Loads a previously saved state snapshot. Restores root key, UMP token, profiles, and active profile.
997
+ * Handles Version 1 (legacy) and Version 2 formats.
998
+ *
999
+ * @param snapshot Encrypted snapshot bytes.
1000
+ */
1001
+ async loadSnapshot(snapshot: number[]): Promise<void> {
1002
+ try {
1003
+ const reader = new Utils.Reader(snapshot)
1004
+ const version = reader.readUInt8()
1005
+
1006
+ let snapshotKey: number[]
1007
+ let encryptedPayload: number[]
1008
+ let activeProfileId = DEFAULT_PROFILE_ID // Default for V1
1009
+
1010
+ if (version === 1) {
1011
+ snapshotKey = reader.read(32)
1012
+ encryptedPayload = reader.read()
1013
+ } else if (version === 2) {
1014
+ snapshotKey = reader.read(32)
1015
+ activeProfileId = reader.read(16) // Read active profile ID
1016
+ encryptedPayload = reader.read()
1017
+ } else {
1018
+ throw new Error(`Unsupported snapshot version: ${version}`)
1019
+ }
1020
+
1021
+ // Decrypt payload
1022
+ const decryptedPayload = new SymmetricKey(snapshotKey).decrypt(encryptedPayload) as number[]
1023
+ const payloadReader = new Utils.Reader(decryptedPayload)
1024
+
1025
+ // Read root primary key
1026
+ const rootPrimaryKey = payloadReader.read(32)
1027
+
1028
+ // Read serialized UMP token
1029
+ const tokenLen = payloadReader.readVarIntNum()
1030
+ const tokenBytes = payloadReader.read(tokenLen)
1031
+ const token = this.deserializeUMPToken(tokenBytes)
1032
+
1033
+ // Assign loaded data
1034
+ this.currentUMPToken = token
1035
+
1036
+ // Setup root infrastructure, load profiles, and switch to the loaded active profile
1037
+ await this.setupRootInfrastructure(rootPrimaryKey) // Will automatically load profiles
1038
+ await this.switchProfile(activeProfileId) // Switch to the profile saved in the snapshot
1039
+
1040
+ this.authenticationFlow = 'existing-user' // Loading implies existing user
1041
+ } catch (error) {
1042
+ this.destroy() // Clear state on error
1043
+ throw new Error(`Failed to load snapshot: ${(error as Error).message}`)
1044
+ }
1045
+ }
1046
+
1047
+ async syncUMPToken(): Promise<boolean> {
1048
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey) {
1049
+ throw new Error('Wallet not authenticated or missing UMP token.')
1050
+ }
1051
+
1052
+ const currentToken = this.currentUMPToken
1053
+ let refreshed: UMPToken | undefined
1054
+
1055
+ if (currentToken.presentationHash && currentToken.presentationHash.length) {
1056
+ refreshed = await this.UMPTokenInteractor.findByPresentationKeyHash(currentToken.presentationHash)
1057
+ }
1058
+
1059
+ if (!refreshed && currentToken.recoveryHash && currentToken.recoveryHash.length) {
1060
+ refreshed = await this.UMPTokenInteractor.findByRecoveryKeyHash(currentToken.recoveryHash)
1061
+ }
1062
+
1063
+ if (!refreshed) {
1064
+ return false
1065
+ }
1066
+
1067
+ if (
1068
+ refreshed.currentOutpoint &&
1069
+ currentToken.currentOutpoint &&
1070
+ refreshed.currentOutpoint === currentToken.currentOutpoint
1071
+ ) {
1072
+ return false
1073
+ }
1074
+
1075
+ this.currentUMPToken = refreshed
1076
+ await this.setupRootInfrastructure(this.rootPrimaryKey)
1077
+ this.saveSnapshot()
1078
+ return true
1079
+ }
1080
+
1081
+ /**
1082
+ * Destroys the wallet state, clearing keys, tokens, and profiles.
1083
+ */
1084
+ destroy(): void {
1085
+ this.underlying = undefined
1086
+ this.rootPrivilegedKeyManager = undefined
1087
+ this.authenticated = false
1088
+ this.rootPrimaryKey = undefined
1089
+ this.currentUMPToken = undefined
1090
+ this.presentationKey = undefined
1091
+ this.recoveryKey = undefined
1092
+ this.profiles = []
1093
+ this.activeProfileId = DEFAULT_PROFILE_ID
1094
+ this.authenticationMode = 'presentation-key-and-password'
1095
+ this.authenticationFlow = 'new-user'
1096
+ }
1097
+
1098
+ // --- Profile Management Methods ---
1099
+
1100
+ /**
1101
+ * Lists all available profiles, including the default profile.
1102
+ * @returns Array of profile info objects, including an 'active' flag.
1103
+ */
1104
+ listProfiles(): Array<{
1105
+ id: number[]
1106
+ name: string
1107
+ createdAt: number | null
1108
+ active: boolean
1109
+ identityKey: string
1110
+ }> {
1111
+ if (!this.authenticated) {
1112
+ throw new Error('Not authenticated.')
1113
+ }
1114
+ const profileList = [
1115
+ // Default profile
1116
+ {
1117
+ id: DEFAULT_PROFILE_ID,
1118
+ name: 'default',
1119
+ createdAt: null, // Default profile doesn't have a creation timestamp in the same way
1120
+ active: this.activeProfileId.every(x => x === 0),
1121
+ identityKey: new PrivateKey(this.rootPrimaryKey).toPublicKey().toString()
1122
+ },
1123
+ // Other profiles
1124
+ ...this.profiles.map(p => ({
1125
+ id: p.id,
1126
+ name: p.name,
1127
+ createdAt: p.createdAt,
1128
+ active: this.activeProfileId.every((x, i) => x === p.id[i]),
1129
+ identityKey: new PrivateKey(this.XOR(this.rootPrimaryKey as number[], p.primaryPad)).toPublicKey().toString()
1130
+ }))
1131
+ ]
1132
+ return profileList
1133
+ }
1134
+
1135
+ /**
1136
+ * Adds a new profile with the given name.
1137
+ * Generates necessary pads and updates the UMP token.
1138
+ * Does not switch to the new profile automatically.
1139
+ *
1140
+ * @param name The desired name for the new profile.
1141
+ * @returns The ID of the newly created profile.
1142
+ */
1143
+ async addProfile(name: string): Promise<number[]> {
1144
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1145
+ throw new Error('Wallet not fully initialized or authenticated.')
1146
+ }
1147
+
1148
+ // Ensure name is unique (including 'default')
1149
+ if (name === 'default' || this.profiles.some(p => p.name.toLowerCase() === name.toLowerCase())) {
1150
+ throw new Error(`Profile name "${name}" is already in use.`)
1151
+ }
1152
+
1153
+ const newProfile: Profile = {
1154
+ name,
1155
+ id: Random(16),
1156
+ primaryPad: Random(32),
1157
+ privilegedPad: Random(32),
1158
+ createdAt: Math.floor(Date.now() / 1000)
1159
+ }
1160
+
1161
+ this.profiles.push(newProfile)
1162
+
1163
+ // Update the UMP token with the new profile list
1164
+ await this.updateAuthFactors(
1165
+ this.currentUMPToken.passwordSalt,
1166
+ // Need to re-derive/decrypt factors needed for re-encryption
1167
+ await this.getFactor('passwordKey'),
1168
+ await this.getFactor('presentationKey'),
1169
+ await this.getFactor('recoveryKey'),
1170
+ this.rootPrimaryKey,
1171
+ await this.getFactor('privilegedKey'), // Get ROOT privileged key
1172
+ this.profiles // Pass the updated profile list
1173
+ )
1174
+
1175
+ return newProfile.id
1176
+ }
1177
+
1178
+ /**
1179
+ * Deletes a profile by its ID.
1180
+ * Cannot delete the default profile. If the active profile is deleted,
1181
+ * it switches back to the default profile.
1182
+ *
1183
+ * @param profileId The 16-byte ID of the profile to delete.
1184
+ */
1185
+ async deleteProfile(profileId: number[]): Promise<void> {
1186
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1187
+ throw new Error('Wallet not fully initialized or authenticated.')
1188
+ }
1189
+ if (profileId.every(x => x === 0)) {
1190
+ throw new Error('Cannot delete the default profile.')
1191
+ }
1192
+
1193
+ const profileIndex = this.profiles.findIndex(p => p.id.every((x, i) => x === profileId[i]))
1194
+ if (profileIndex === -1) {
1195
+ throw new Error('Profile not found.')
1196
+ }
1197
+
1198
+ // Remove the profile
1199
+ this.profiles.splice(profileIndex, 1)
1200
+
1201
+ // If the deleted profile was active, switch to default
1202
+ if (this.activeProfileId.every((x, i) => x === profileId[i])) {
1203
+ await this.switchProfile(DEFAULT_PROFILE_ID) // This rebuilds the wallet
1204
+ }
1205
+
1206
+ // Update the UMP token
1207
+ await this.updateAuthFactors(
1208
+ this.currentUMPToken.passwordSalt,
1209
+ await this.getFactor('passwordKey'),
1210
+ await this.getFactor('presentationKey'),
1211
+ await this.getFactor('recoveryKey'),
1212
+ this.rootPrimaryKey,
1213
+ await this.getFactor('privilegedKey'), // Get ROOT privileged key
1214
+ this.profiles // Pass updated list
1215
+ )
1216
+ }
1217
+
1218
+ /**
1219
+ * Switches the active profile. This re-derives keys and rebuilds the underlying wallet.
1220
+ *
1221
+ * @param profileId The 16-byte ID of the profile to switch to (use DEFAULT_PROFILE_ID for default).
1222
+ */
1223
+ async switchProfile(profileId: number[]): Promise<void> {
1224
+ if (!this.authenticated || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1225
+ throw new Error('Cannot switch profile: Wallet not authenticated or root keys missing.')
1226
+ }
1227
+
1228
+ let profilePrimaryKey: number[]
1229
+ let profilePrivilegedPad: number[] | undefined // Pad for the target profile
1230
+
1231
+ if (profileId.every(x => x === 0)) {
1232
+ // Switching to default profile
1233
+ profilePrimaryKey = this.rootPrimaryKey
1234
+ profilePrivilegedPad = undefined // No pad for default
1235
+ this.activeProfileId = DEFAULT_PROFILE_ID
1236
+ } else {
1237
+ // Switching to a non-default profile
1238
+ const profile = this.profiles.find(p => p.id.every((x, i) => x === profileId[i]))
1239
+ if (!profile) {
1240
+ throw new Error('Profile not found.')
1241
+ }
1242
+ profilePrimaryKey = this.XOR(this.rootPrimaryKey, profile.primaryPad)
1243
+ profilePrivilegedPad = profile.privilegedPad
1244
+ this.activeProfileId = profileId
1245
+ }
1246
+
1247
+ // Create a *profile-specific* PrivilegedKeyManager.
1248
+ // It uses the ROOT manager internally but applies the profile's pad.
1249
+ const profilePrivilegedKeyManager = new PrivilegedKeyManager(async (reason: string) => {
1250
+ // Request the ROOT privileged key using the root manager
1251
+ const rootPrivileged: PrivateKey = await (this.rootPrivilegedKeyManager as any).getPrivilegedKey(reason)
1252
+ const rootPrivilegedBytes = rootPrivileged.toArray()
1253
+
1254
+ // Apply the profile's pad if applicable
1255
+ const profilePrivilegedBytes = profilePrivilegedPad
1256
+ ? this.XOR(rootPrivilegedBytes, profilePrivilegedPad)
1257
+ : rootPrivilegedBytes
1258
+
1259
+ return new PrivateKey(profilePrivilegedBytes)
1260
+ })
1261
+
1262
+ // Build the underlying wallet for the specific profile
1263
+ this.underlying = await this.walletBuilder(
1264
+ profilePrimaryKey,
1265
+ profilePrivilegedKeyManager, // Pass the profile-specific manager
1266
+ this.activeProfileId // Pass the ID of the profile being activated
1267
+ )
1268
+ }
1269
+
1270
+ // --- Key Management Methods ---
1271
+
1272
+ /**
1273
+ * Changes the user's password. Re-wraps keys and updates the UMP token.
1274
+ */
1275
+ async changePassword(newPassword: string): Promise<void> {
1276
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1277
+ throw new Error('Not authenticated or missing required data.')
1278
+ }
1279
+
1280
+ const passwordSalt = Random(32)
1281
+ const newPasswordKey = await pbkdf2NativeOrJs(
1282
+ Utils.toArray(newPassword, 'utf8'),
1283
+ passwordSalt,
1284
+ PBKDF2_NUM_ROUNDS,
1285
+ 32,
1286
+ 'sha512'
1287
+ )
1288
+
1289
+ // Decrypt existing factors needed for re-encryption, using the *root* privileged key manager
1290
+ const recoveryKey = await this.getFactor('recoveryKey')
1291
+ const presentationKey = await this.getFactor('presentationKey')
1292
+ const rootPrivilegedKey = await this.getFactor('privilegedKey') // Get ROOT privileged key
1293
+
1294
+ await this.updateAuthFactors(
1295
+ passwordSalt,
1296
+ newPasswordKey,
1297
+ presentationKey,
1298
+ recoveryKey,
1299
+ this.rootPrimaryKey,
1300
+ rootPrivilegedKey, // Pass the explicitly fetched root key
1301
+ this.profiles // Preserve existing profiles
1302
+ )
1303
+ }
1304
+
1305
+ /**
1306
+ * Retrieves the current recovery key. Requires privileged access.
1307
+ */
1308
+ async getRecoveryKey(): Promise<number[]> {
1309
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1310
+ throw new Error('Not authenticated or missing required data.')
1311
+ }
1312
+ return this.getFactor('recoveryKey')
1313
+ }
1314
+
1315
+ /**
1316
+ * Changes the user's recovery key. Prompts user to save the new key.
1317
+ */
1318
+ async changeRecoveryKey(): Promise<void> {
1319
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1320
+ throw new Error('Not authenticated or missing required data.')
1321
+ }
1322
+
1323
+ // Decrypt existing factors needed
1324
+ const passwordKey = await this.getFactor('passwordKey')
1325
+ const presentationKey = await this.getFactor('presentationKey')
1326
+ const rootPrivilegedKey = await this.getFactor('privilegedKey') // Get ROOT privileged key
1327
+
1328
+ // Generate and save new recovery key
1329
+ const newRecoveryKey = Random(32)
1330
+ await this.recoveryKeySaver(newRecoveryKey)
1331
+
1332
+ await this.updateAuthFactors(
1333
+ this.currentUMPToken.passwordSalt,
1334
+ passwordKey,
1335
+ presentationKey,
1336
+ newRecoveryKey, // Use the new key
1337
+ this.rootPrimaryKey,
1338
+ rootPrivilegedKey,
1339
+ this.profiles // Preserve profiles
1340
+ )
1341
+ }
1342
+
1343
+ /**
1344
+ * Changes the user's presentation key.
1345
+ */
1346
+ async changePresentationKey(newPresentationKey: number[]): Promise<void> {
1347
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1348
+ throw new Error('Not authenticated or missing required data.')
1349
+ }
1350
+ if (newPresentationKey.length !== 32) {
1351
+ throw new Error('Presentation key must be 32 bytes.')
1352
+ }
1353
+
1354
+ // Decrypt existing factors
1355
+ const recoveryKey = await this.getFactor('recoveryKey')
1356
+ const passwordKey = await this.getFactor('passwordKey')
1357
+ const rootPrivilegedKey = await this.getFactor('privilegedKey') // Get ROOT privileged key
1358
+
1359
+ await this.updateAuthFactors(
1360
+ this.currentUMPToken.passwordSalt,
1361
+ passwordKey,
1362
+ newPresentationKey, // Use the new key
1363
+ recoveryKey,
1364
+ this.rootPrimaryKey,
1365
+ rootPrivilegedKey,
1366
+ this.profiles // Preserve profiles
1367
+ )
1368
+ // Update the temporarily stored key if it was set
1369
+ if (this.presentationKey) {
1370
+ this.presentationKey = newPresentationKey
1371
+ }
1372
+ }
1373
+
1374
+ // --- Internal Helper Methods ---
1375
+
1376
+ /**
1377
+ * Performs XOR operation on two byte arrays.
1378
+ */
1379
+ private XOR(n1: number[], n2: number[]): number[] {
1380
+ if (n1.length !== n2.length) {
1381
+ // Provide more context in error
1382
+ throw new Error(`XOR length mismatch: ${n1.length} vs ${n2.length}`)
1383
+ }
1384
+ const r = new Array<number>(n1.length)
1385
+ for (let i = 0; i < n1.length; i++) {
1386
+ r[i] = n1[i] ^ n2[i]
1387
+ }
1388
+ return r
1389
+ }
1390
+
1391
+ /**
1392
+ * Helper to decrypt a specific factor (key) stored encrypted in the UMP token.
1393
+ * Requires the root privileged key manager.
1394
+ * @param factorName Name of the factor to decrypt ('passwordKey', 'presentationKey', 'recoveryKey', 'privilegedKey').
1395
+ * @param getRoot If true and factorName is 'privilegedKey', returns the root privileged key bytes directly.
1396
+ * @returns The decrypted key bytes.
1397
+ */
1398
+ private async getFactor(
1399
+ factorName: 'passwordKey' | 'presentationKey' | 'recoveryKey' | 'privilegedKey'
1400
+ ): Promise<number[]> {
1401
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1402
+ throw new Error(`Cannot get factor "${factorName}": Wallet not ready.`)
1403
+ }
1404
+
1405
+ const protocolID: [0 | 1 | 2, string] = [2, 'admin key wrapping'] // Protocol used for encrypting factors
1406
+ const keyID = '1' // Key ID used
1407
+
1408
+ try {
1409
+ switch (factorName) {
1410
+ case 'passwordKey':
1411
+ return (
1412
+ await this.rootPrivilegedKeyManager.decrypt({
1413
+ ciphertext: this.currentUMPToken.passwordKeyEncrypted,
1414
+ protocolID,
1415
+ keyID
1416
+ })
1417
+ ).plaintext
1418
+ case 'presentationKey':
1419
+ return (
1420
+ await this.rootPrivilegedKeyManager.decrypt({
1421
+ ciphertext: this.currentUMPToken.presentationKeyEncrypted,
1422
+ protocolID,
1423
+ keyID
1424
+ })
1425
+ ).plaintext
1426
+ case 'recoveryKey':
1427
+ return (
1428
+ await this.rootPrivilegedKeyManager.decrypt({
1429
+ ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
1430
+ protocolID,
1431
+ keyID
1432
+ })
1433
+ ).plaintext
1434
+ case 'privilegedKey': {
1435
+ // This needs careful handling based on whether the ROOT or PROFILE key is needed.
1436
+ // This helper is mostly used for UMP updates, which need the ROOT key.
1437
+ // We retrieve the PrivateKey object first.
1438
+ const pk = await (this.rootPrivilegedKeyManager as any).getPrivilegedKey('UMP token update', true) // Force retrieval of root key
1439
+ return pk.toArray() // Return bytes
1440
+ }
1441
+ default:
1442
+ throw new Error(`Unknown factor name: ${factorName}`)
1443
+ }
1444
+ } catch (error) {
1445
+ console.error(`Error decrypting factor ${factorName}:`, error)
1446
+ throw new Error(`Failed to decrypt factor "${factorName}": ${(error as Error).message}`)
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * Recomputes UMP token fields with updated factors and profiles, then publishes the update.
1452
+ * This operation requires the *root* privileged key and the *default* profile wallet.
1453
+ */
1454
+ private async updateAuthFactors(
1455
+ passwordSalt: number[],
1456
+ passwordKey: number[],
1457
+ presentationKey: number[],
1458
+ recoveryKey: number[],
1459
+ rootPrimaryKey: number[],
1460
+ rootPrivilegedKey: number[], // Explicitly pass the root key bytes
1461
+ profiles?: Profile[] // Pass current/new profiles list
1462
+ ): Promise<void> {
1463
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken) {
1464
+ throw new Error('Wallet is not properly authenticated or missing data for update.')
1465
+ }
1466
+ // Ensure we have the OLD token to consume
1467
+ const oldTokenToConsume = { ...this.currentUMPToken }
1468
+ if (!oldTokenToConsume.currentOutpoint) {
1469
+ throw new Error('Cannot update UMP token: Old token has no outpoint.')
1470
+ }
1471
+
1472
+ // Derive symmetrical encryption keys using XOR for the *root* keys
1473
+ const presentationPassword = new SymmetricKey(this.XOR(presentationKey, passwordKey))
1474
+ const presentationRecovery = new SymmetricKey(this.XOR(presentationKey, recoveryKey))
1475
+ const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
1476
+ const primaryPassword = new SymmetricKey(this.XOR(rootPrimaryKey, passwordKey)) // Use rootPrimaryKey
1477
+
1478
+ // Build a temporary privileged key manager using the explicit ROOT privileged key
1479
+ const tempRootPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(rootPrivilegedKey))
1480
+
1481
+ // Encrypt profiles if provided
1482
+ let profilesEncrypted: number[] | undefined
1483
+ if (profiles && profiles.length > 0) {
1484
+ const profilesJson = JSON.stringify(profiles)
1485
+ const profilesBytes = Utils.toArray(profilesJson, 'utf8')
1486
+ profilesEncrypted = new SymmetricKey(rootPrimaryKey).encrypt(profilesBytes) as number[]
1487
+ }
1488
+
1489
+ // Construct the new UMP token data
1490
+ const newTokenData: UMPToken = {
1491
+ passwordSalt,
1492
+ passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey) as number[],
1493
+ passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey) as number[],
1494
+ presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey) as number[],
1495
+ passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey) as number[],
1496
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey) as number[],
1497
+ presentationHash: Hash.sha256(presentationKey),
1498
+ recoveryHash: Hash.sha256(recoveryKey),
1499
+ presentationKeyEncrypted: (
1500
+ await tempRootPrivilegedKeyManager.encrypt({
1501
+ plaintext: presentationKey,
1502
+ protocolID: [2, 'admin key wrapping'],
1503
+ keyID: '1'
1504
+ })
1505
+ ).ciphertext,
1506
+ passwordKeyEncrypted: (
1507
+ await tempRootPrivilegedKeyManager.encrypt({
1508
+ plaintext: passwordKey,
1509
+ protocolID: [2, 'admin key wrapping'],
1510
+ keyID: '1'
1511
+ })
1512
+ ).ciphertext,
1513
+ recoveryKeyEncrypted: (
1514
+ await tempRootPrivilegedKeyManager.encrypt({
1515
+ plaintext: recoveryKey,
1516
+ protocolID: [2, 'admin key wrapping'],
1517
+ keyID: '1'
1518
+ })
1519
+ ).ciphertext,
1520
+ profilesEncrypted // Add encrypted profiles
1521
+ // currentOutpoint will be set after publishing
1522
+ }
1523
+
1524
+ // We need the wallet built for the DEFAULT profile to publish the UMP token.
1525
+ // If the current active profile is not default, temporarily switch, publish, then switch back.
1526
+ const currentActiveId = this.activeProfileId
1527
+ let walletToUse: WalletInterface | undefined = this.underlying
1528
+
1529
+ if (!currentActiveId.every(x => x === 0)) {
1530
+ console.log('Temporarily switching to default profile to update UMP token...')
1531
+ await this.switchProfile(DEFAULT_PROFILE_ID) // This rebuilds this.underlying
1532
+ walletToUse = this.underlying
1533
+ }
1534
+
1535
+ if (!walletToUse) {
1536
+ throw new Error('Default profile wallet could not be activated for UMP token update.')
1537
+ }
1538
+
1539
+ // Publish the new token on-chain, consuming the old one
1540
+ try {
1541
+ newTokenData.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
1542
+ walletToUse,
1543
+ this.adminOriginator,
1544
+ newTokenData,
1545
+ oldTokenToConsume // Consume the previous token
1546
+ )
1547
+ // Update the manager's state
1548
+ this.currentUMPToken = newTokenData
1549
+ // Profiles are already updated in this.profiles if they were passed in
1550
+ } finally {
1551
+ // Switch back if we temporarily switched
1552
+ if (!currentActiveId.every(x => x === 0)) {
1553
+ console.log('Switching back to original profile...')
1554
+ await this.switchProfile(currentActiveId)
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ /**
1560
+ * Serializes a UMP token to binary format (Version 2 with optional profiles).
1561
+ * Layout: [1 byte version=2] + [11 * (varint len + bytes) for standard fields] + [1 byte profile_flag] + [IF flag=1 THEN varint len + profile bytes] + [varint len + outpoint bytes]
1562
+ */
1563
+ private serializeUMPToken(token: UMPToken): number[] {
1564
+ if (!token.currentOutpoint) {
1565
+ throw new Error('Token must have outpoint for serialization')
1566
+ }
1567
+
1568
+ const writer = new Utils.Writer()
1569
+ writer.writeUInt8(2) // Version 2
1570
+
1571
+ const writeArray = (arr: number[]) => {
1572
+ writer.writeVarIntNum(arr.length)
1573
+ writer.write(arr)
1574
+ }
1575
+
1576
+ // Write standard fields in specific order
1577
+ writeArray(token.passwordSalt) // 0
1578
+ writeArray(token.passwordPresentationPrimary) // 1
1579
+ writeArray(token.passwordRecoveryPrimary) // 2
1580
+ writeArray(token.presentationRecoveryPrimary) // 3
1581
+ writeArray(token.passwordPrimaryPrivileged) // 4
1582
+ writeArray(token.presentationRecoveryPrivileged) // 5
1583
+ writeArray(token.presentationHash) // 6
1584
+ writeArray(token.recoveryHash) // 7
1585
+ writeArray(token.presentationKeyEncrypted) // 8
1586
+ writeArray(token.passwordKeyEncrypted) // 9 - Swapped order vs original doc comment
1587
+ writeArray(token.recoveryKeyEncrypted) // 10
1588
+
1589
+ // Write optional profiles field
1590
+ if (token.profilesEncrypted && token.profilesEncrypted.length > 0) {
1591
+ writer.writeUInt8(1) // Flag indicating profiles present
1592
+ writeArray(token.profilesEncrypted)
1593
+ } else {
1594
+ writer.writeUInt8(0) // Flag indicating no profiles
1595
+ }
1596
+
1597
+ // Write outpoint string
1598
+ const outpointBytes = Utils.toArray(token.currentOutpoint, 'utf8')
1599
+ writer.writeVarIntNum(outpointBytes.length)
1600
+ writer.write(outpointBytes)
1601
+
1602
+ return writer.toArray()
1603
+ }
1604
+
1605
+ /**
1606
+ * Deserializes a UMP token from binary format (Handles Version 1 and 2).
1607
+ */
1608
+ private deserializeUMPToken(bin: number[]): UMPToken {
1609
+ const reader = new Utils.Reader(bin)
1610
+ const version = reader.readUInt8()
1611
+
1612
+ if (version !== 1 && version !== 2) {
1613
+ throw new Error(`Unsupported UMP token serialization version: ${version}`)
1614
+ }
1615
+
1616
+ const readArray = (): number[] => {
1617
+ const length = reader.readVarIntNum()
1618
+ return reader.read(length)
1619
+ }
1620
+
1621
+ // Read standard fields (order matches serialization V2)
1622
+ const passwordSalt = readArray() // 0
1623
+ const passwordPresentationPrimary = readArray() // 1
1624
+ const passwordRecoveryPrimary = readArray() // 2
1625
+ const presentationRecoveryPrimary = readArray() // 3
1626
+ const passwordPrimaryPrivileged = readArray() // 4
1627
+ const presentationRecoveryPrivileged = readArray() // 5
1628
+ const presentationHash = readArray() // 6
1629
+ const recoveryHash = readArray() // 7
1630
+ const presentationKeyEncrypted = readArray() // 8
1631
+ const passwordKeyEncrypted = readArray() // 9
1632
+ const recoveryKeyEncrypted = readArray() // 10
1633
+
1634
+ // Read optional profiles (only in V2)
1635
+ let profilesEncrypted: number[] | undefined
1636
+ if (version === 2) {
1637
+ const profilesFlag = reader.readUInt8()
1638
+ if (profilesFlag === 1) {
1639
+ profilesEncrypted = readArray()
1640
+ }
1641
+ }
1642
+
1643
+ // Read outpoint string
1644
+ const outpointLen = reader.readVarIntNum()
1645
+ const outpointBytes = reader.read(outpointLen)
1646
+ const currentOutpoint = Utils.toUTF8(outpointBytes)
1647
+
1648
+ const token: UMPToken = {
1649
+ passwordSalt,
1650
+ passwordPresentationPrimary,
1651
+ passwordRecoveryPrimary,
1652
+ presentationRecoveryPrimary,
1653
+ passwordPrimaryPrivileged,
1654
+ presentationRecoveryPrivileged,
1655
+ presentationHash,
1656
+ recoveryHash,
1657
+ presentationKeyEncrypted,
1658
+ passwordKeyEncrypted, // Corrected order
1659
+ recoveryKeyEncrypted,
1660
+ profilesEncrypted, // May be undefined
1661
+ currentOutpoint
1662
+ }
1663
+
1664
+ return token
1665
+ }
1666
+
1667
+ /**
1668
+ * Sets up the root key infrastructure after authentication or loading from snapshot.
1669
+ * Initializes the root primary key, root privileged key manager, loads profiles,
1670
+ * and sets the authenticated flag. Does NOT switch profile initially.
1671
+ *
1672
+ * @param rootPrimaryKey The user's root primary key (32 bytes).
1673
+ * @param ephemeralRootPrivilegedKey Optional root privileged key (e.g., during recovery flows).
1674
+ */
1675
+ private async setupRootInfrastructure(
1676
+ rootPrimaryKey: number[],
1677
+ ephemeralRootPrivilegedKey?: number[]
1678
+ ): Promise<void> {
1679
+ if (!this.currentUMPToken) {
1680
+ throw new Error('A UMP token must exist before setting up root infrastructure!')
1681
+ }
1682
+ this.rootPrimaryKey = rootPrimaryKey
1683
+
1684
+ // Store ephemeral key if provided, for one-time use by the manager
1685
+ let oneTimePrivilegedKey: PrivateKey | undefined = ephemeralRootPrivilegedKey
1686
+ ? new PrivateKey(ephemeralRootPrivilegedKey)
1687
+ : undefined
1688
+
1689
+ // Create the ROOT PrivilegedKeyManager
1690
+ this.rootPrivilegedKeyManager = new PrivilegedKeyManager(async (reason: string) => {
1691
+ // 1. Use one-time key if available (for recovery)
1692
+ if (oneTimePrivilegedKey) {
1693
+ const tempKey = oneTimePrivilegedKey
1694
+ oneTimePrivilegedKey = undefined // Consume it
1695
+ return tempKey
1696
+ }
1697
+
1698
+ // 2. Otherwise, derive from password
1699
+ const password = await this.passwordRetriever(reason, (passwordCandidate: string) => {
1700
+ try {
1701
+ const derivedPasswordKey = Hash.pbkdf2(
1702
+ Utils.toArray(passwordCandidate, 'utf8'),
1703
+ this.currentUMPToken!.passwordSalt,
1704
+ PBKDF2_NUM_ROUNDS,
1705
+ 32,
1706
+ 'sha512'
1707
+ )
1708
+ const privilegedDecryptor = this.XOR(this.rootPrimaryKey!, derivedPasswordKey)
1709
+ const decryptedPrivileged = new SymmetricKey(privilegedDecryptor).decrypt(
1710
+ this.currentUMPToken!.passwordPrimaryPrivileged
1711
+ ) as number[]
1712
+ return !!decryptedPrivileged // Test passes if decryption works
1713
+ } catch (e) {
1714
+ return false
1715
+ }
1716
+ })
1717
+
1718
+ // Decrypt the root privileged key using the confirmed password
1719
+ const derivedPasswordKey = await pbkdf2NativeOrJs(
1720
+ Utils.toArray(password, 'utf8'),
1721
+ this.currentUMPToken!.passwordSalt,
1722
+ PBKDF2_NUM_ROUNDS,
1723
+ 32,
1724
+ 'sha512'
1725
+ )
1726
+ const privilegedDecryptor = this.XOR(this.rootPrimaryKey!, derivedPasswordKey)
1727
+ const rootPrivilegedBytes = new SymmetricKey(privilegedDecryptor).decrypt(
1728
+ this.currentUMPToken!.passwordPrimaryPrivileged
1729
+ ) as number[]
1730
+
1731
+ return new PrivateKey(rootPrivilegedBytes) // Return the ROOT key object
1732
+ })
1733
+
1734
+ // Decrypt and load profiles if present in the token
1735
+ this.profiles = [] // Clear existing profiles before loading
1736
+ if (this.currentUMPToken.profilesEncrypted && this.currentUMPToken.profilesEncrypted.length > 0) {
1737
+ try {
1738
+ const decryptedProfileBytes = new SymmetricKey(rootPrimaryKey).decrypt(
1739
+ this.currentUMPToken.profilesEncrypted
1740
+ ) as number[]
1741
+ const profilesJson = Utils.toUTF8(decryptedProfileBytes)
1742
+ this.profiles = JSON.parse(profilesJson) as Profile[]
1743
+ } catch (error) {
1744
+ console.error('Failed to decrypt or parse profiles:', error)
1745
+ // Decide if this should be fatal or just log and continue without profiles
1746
+ this.profiles = [] // Ensure profiles are empty on error
1747
+ // Optionally re-throw or handle more gracefully
1748
+ throw new Error(`Failed to load profiles: ${(error as Error).message}`)
1749
+ }
1750
+ }
1751
+
1752
+ this.authenticated = true
1753
+ // Note: We don't call switchProfile here anymore.
1754
+ // It's called by the auth methods (providePassword/provideRecoveryKey) or loadSnapshot after this.
1755
+ }
1756
+
1757
+ /*
1758
+ * ---------------------------------------------------------------------------------------
1759
+ * Standard WalletInterface methods proxying to the *active* underlying wallet.
1760
+ * Includes authentication checks and admin originator protection.
1761
+ * ---------------------------------------------------------------------------------------
1762
+ */
1763
+
1764
+ private checkAuthAndUnderlying(originator?: string): void {
1765
+ if (!this.authenticated) {
1766
+ throw new Error('User is not authenticated.')
1767
+ }
1768
+ if (!this.underlying) {
1769
+ // This might happen if authentication succeeded but profile switching failed
1770
+ throw new Error('Underlying wallet for the active profile is not initialized.')
1771
+ }
1772
+ if (originator === this.adminOriginator) {
1773
+ throw new Error('External applications are not allowed to use the admin originator.')
1774
+ }
1775
+ }
1776
+
1777
+ // Example proxy method (repeat pattern for all others)
1778
+ async getPublicKey(
1779
+ args: GetPublicKeyArgs,
1780
+ originator?: OriginatorDomainNameStringUnder250Bytes
1781
+ ): Promise<GetPublicKeyResult> {
1782
+ this.checkAuthAndUnderlying(originator)
1783
+ return this.underlying!.getPublicKey(args, originator)
1784
+ }
1785
+
1786
+ async revealCounterpartyKeyLinkage(
1787
+ args: RevealCounterpartyKeyLinkageArgs,
1788
+ originator?: OriginatorDomainNameStringUnder250Bytes
1789
+ ): Promise<RevealCounterpartyKeyLinkageResult> {
1790
+ this.checkAuthAndUnderlying(originator)
1791
+ return this.underlying!.revealCounterpartyKeyLinkage(args, originator)
1792
+ }
1793
+
1794
+ async revealSpecificKeyLinkage(
1795
+ args: RevealSpecificKeyLinkageArgs,
1796
+ originator?: OriginatorDomainNameStringUnder250Bytes
1797
+ ): Promise<RevealSpecificKeyLinkageResult> {
1798
+ this.checkAuthAndUnderlying(originator)
1799
+ return this.underlying!.revealSpecificKeyLinkage(args, originator)
1800
+ }
1801
+
1802
+ async encrypt(
1803
+ args: WalletEncryptArgs,
1804
+ originator?: OriginatorDomainNameStringUnder250Bytes
1805
+ ): Promise<WalletEncryptResult> {
1806
+ this.checkAuthAndUnderlying(originator)
1807
+ return this.underlying!.encrypt(args, originator)
1808
+ }
1809
+
1810
+ async decrypt(
1811
+ args: WalletDecryptArgs,
1812
+ originator?: OriginatorDomainNameStringUnder250Bytes
1813
+ ): Promise<WalletDecryptResult> {
1814
+ this.checkAuthAndUnderlying(originator)
1815
+ return this.underlying!.decrypt(args, originator)
1816
+ }
1817
+
1818
+ async createHmac(
1819
+ args: CreateHmacArgs,
1820
+ originator?: OriginatorDomainNameStringUnder250Bytes
1821
+ ): Promise<CreateHmacResult> {
1822
+ this.checkAuthAndUnderlying(originator)
1823
+ return this.underlying!.createHmac(args, originator)
1824
+ }
1825
+
1826
+ async verifyHmac(
1827
+ args: VerifyHmacArgs,
1828
+ originator?: OriginatorDomainNameStringUnder250Bytes
1829
+ ): Promise<VerifyHmacResult> {
1830
+ this.checkAuthAndUnderlying(originator)
1831
+ return this.underlying!.verifyHmac(args, originator)
1832
+ }
1833
+
1834
+ async createSignature(
1835
+ args: CreateSignatureArgs,
1836
+ originator?: OriginatorDomainNameStringUnder250Bytes
1837
+ ): Promise<CreateSignatureResult> {
1838
+ this.checkAuthAndUnderlying(originator)
1839
+ return this.underlying!.createSignature(args, originator)
1840
+ }
1841
+
1842
+ async verifySignature(
1843
+ args: VerifySignatureArgs,
1844
+ originator?: OriginatorDomainNameStringUnder250Bytes
1845
+ ): Promise<VerifySignatureResult> {
1846
+ this.checkAuthAndUnderlying(originator)
1847
+ return this.underlying!.verifySignature(args, originator)
1848
+ }
1849
+
1850
+ async createAction(
1851
+ args: CreateActionArgs,
1852
+ originator?: OriginatorDomainNameStringUnder250Bytes
1853
+ ): Promise<CreateActionResult> {
1854
+ this.checkAuthAndUnderlying(originator)
1855
+ return this.underlying!.createAction(args, originator)
1856
+ }
1857
+
1858
+ async signAction(
1859
+ args: SignActionArgs,
1860
+ originator?: OriginatorDomainNameStringUnder250Bytes
1861
+ ): Promise<SignActionResult> {
1862
+ this.checkAuthAndUnderlying(originator)
1863
+ return this.underlying!.signAction(args, originator)
1864
+ }
1865
+
1866
+ async abortAction(
1867
+ args: AbortActionArgs,
1868
+ originator?: OriginatorDomainNameStringUnder250Bytes
1869
+ ): Promise<AbortActionResult> {
1870
+ this.checkAuthAndUnderlying(originator)
1871
+ return this.underlying!.abortAction(args, originator)
1872
+ }
1873
+
1874
+ async listActions(
1875
+ args: ListActionsArgs,
1876
+ originator?: OriginatorDomainNameStringUnder250Bytes
1877
+ ): Promise<ListActionsResult> {
1878
+ this.checkAuthAndUnderlying(originator)
1879
+ return this.underlying!.listActions(args, originator)
1880
+ }
1881
+
1882
+ async internalizeAction(
1883
+ args: InternalizeActionArgs,
1884
+ originator?: OriginatorDomainNameStringUnder250Bytes
1885
+ ): Promise<InternalizeActionResult> {
1886
+ this.checkAuthAndUnderlying(originator)
1887
+ return this.underlying!.internalizeAction(args, originator)
1888
+ }
1889
+
1890
+ async listOutputs(
1891
+ args: ListOutputsArgs,
1892
+ originator?: OriginatorDomainNameStringUnder250Bytes
1893
+ ): Promise<ListOutputsResult> {
1894
+ this.checkAuthAndUnderlying(originator)
1895
+ return this.underlying!.listOutputs(args, originator)
1896
+ }
1897
+
1898
+ async relinquishOutput(
1899
+ args: RelinquishOutputArgs,
1900
+ originator?: OriginatorDomainNameStringUnder250Bytes
1901
+ ): Promise<RelinquishOutputResult> {
1902
+ this.checkAuthAndUnderlying(originator)
1903
+ return this.underlying!.relinquishOutput(args, originator)
1904
+ }
1905
+
1906
+ async acquireCertificate(
1907
+ args: AcquireCertificateArgs,
1908
+ originator?: OriginatorDomainNameStringUnder250Bytes
1909
+ ): Promise<AcquireCertificateResult> {
1910
+ this.checkAuthAndUnderlying(originator)
1911
+ return this.underlying!.acquireCertificate(args, originator)
1912
+ }
1913
+
1914
+ async listCertificates(
1915
+ args: ListCertificatesArgs,
1916
+ originator?: OriginatorDomainNameStringUnder250Bytes
1917
+ ): Promise<ListCertificatesResult> {
1918
+ this.checkAuthAndUnderlying(originator)
1919
+ return this.underlying!.listCertificates(args, originator)
1920
+ }
1921
+
1922
+ async proveCertificate(
1923
+ args: ProveCertificateArgs,
1924
+ originator?: OriginatorDomainNameStringUnder250Bytes
1925
+ ): Promise<ProveCertificateResult> {
1926
+ this.checkAuthAndUnderlying(originator)
1927
+ return this.underlying!.proveCertificate(args, originator)
1928
+ }
1929
+
1930
+ async relinquishCertificate(
1931
+ args: RelinquishCertificateArgs,
1932
+ originator?: OriginatorDomainNameStringUnder250Bytes
1933
+ ): Promise<RelinquishCertificateResult> {
1934
+ this.checkAuthAndUnderlying(originator)
1935
+ return this.underlying!.relinquishCertificate(args, originator)
1936
+ }
1937
+
1938
+ async discoverByIdentityKey(
1939
+ args: DiscoverByIdentityKeyArgs,
1940
+ originator?: OriginatorDomainNameStringUnder250Bytes
1941
+ ): Promise<DiscoverCertificatesResult> {
1942
+ this.checkAuthAndUnderlying(originator)
1943
+ return this.underlying!.discoverByIdentityKey(args, originator)
1944
+ }
1945
+
1946
+ async discoverByAttributes(
1947
+ args: DiscoverByAttributesArgs,
1948
+ originator?: OriginatorDomainNameStringUnder250Bytes
1949
+ ): Promise<DiscoverCertificatesResult> {
1950
+ this.checkAuthAndUnderlying(originator)
1951
+ return this.underlying!.discoverByAttributes(args, originator)
1952
+ }
1953
+
1954
+ async isAuthenticated(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<AuthenticatedResult> {
1955
+ if (!this.authenticated) {
1956
+ throw new Error('User is not authenticated.')
1957
+ }
1958
+ if (originator === this.adminOriginator) {
1959
+ throw new Error('External applications are not allowed to use the admin originator.')
1960
+ }
1961
+ return { authenticated: true }
1962
+ }
1963
+
1964
+ async waitForAuthentication(
1965
+ _: {},
1966
+ originator?: OriginatorDomainNameStringUnder250Bytes
1967
+ ): Promise<AuthenticatedResult> {
1968
+ if (originator === this.adminOriginator) {
1969
+ throw new Error('External applications are not allowed to use the admin originator.')
1970
+ }
1971
+ while (!this.authenticated || !this.underlying) {
1972
+ await new Promise(resolve => setTimeout(resolve, 100))
1973
+ }
1974
+ return await this.underlying.waitForAuthentication({}, originator)
1975
+ }
1976
+
1977
+ async getHeight(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetHeightResult> {
1978
+ this.checkAuthAndUnderlying(originator)
1979
+ return this.underlying!.getHeight({}, originator)
1980
+ }
1981
+
1982
+ async getHeaderForHeight(
1983
+ args: GetHeaderArgs,
1984
+ originator?: OriginatorDomainNameStringUnder250Bytes
1985
+ ): Promise<GetHeaderResult> {
1986
+ this.checkAuthAndUnderlying(originator)
1987
+ return this.underlying!.getHeaderForHeight(args, originator)
1988
+ }
1989
+
1990
+ async getNetwork(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetNetworkResult> {
1991
+ this.checkAuthAndUnderlying(originator)
1992
+ return this.underlying!.getNetwork({}, originator)
1993
+ }
1994
+
1995
+ async getVersion(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetVersionResult> {
1996
+ this.checkAuthAndUnderlying(originator)
1997
+ return this.underlying!.getVersion({}, originator)
1998
+ }
1999
+ }