@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,2526 @@
1
+ import * as dotenv from 'dotenv'
2
+ import path from 'path'
3
+ import { promises as fsp } from 'fs'
4
+ import { Knex, knex as makeKnex } from 'knex'
5
+ import {
6
+ Beef,
7
+ CachedKeyDeriver,
8
+ CreateActionArgs,
9
+ CreateActionOutput,
10
+ CreateActionResult,
11
+ HexString,
12
+ KeyDeriverApi,
13
+ P2PKH,
14
+ PrivateKey,
15
+ PublicKey,
16
+ SatoshiValue,
17
+ SignActionArgs,
18
+ SignActionResult,
19
+ Transaction,
20
+ Utils,
21
+ WalletAction,
22
+ WalletActionInput,
23
+ WalletActionOutput,
24
+ WalletCertificate,
25
+ WalletInterface
26
+ } from '@bsv/sdk'
27
+ import { StorageIdb } from '../../src/storage/StorageIdb'
28
+ import { Chain, TransactionStatus } from '../../src/sdk/types'
29
+ import { Setup } from '../../src/Setup'
30
+ import { StorageKnex } from '../../src/storage/StorageKnex'
31
+ import { Services } from '../../src/services/Services'
32
+ import { WERR_INSUFFICIENT_FUNDS, WERR_INVALID_PARAMETER } from '../../src/sdk/WERR_errors'
33
+ import { TrxToken, WalletStorageProvider } from '../../src/sdk/WalletStorage.interfaces'
34
+ import { WalletStorageManager } from '../../src/storage/WalletStorageManager'
35
+ import { GetMerklePathResult, PostBeefResult, WalletServicesOptions } from '../../src/sdk/WalletServices.interfaces'
36
+ import { Monitor } from '../../src/monitor/Monitor'
37
+ import { PrivilegedKeyManager } from '../../src/sdk/PrivilegedKeyManager'
38
+ import { Wallet } from '../../src/Wallet'
39
+ import { StorageClient } from '../../src/storage/remoting/StorageClient'
40
+ import { randomBytesBase64, randomBytesHex, verifyOne, verifyTruthy } from '../../src/utility/utilityHelpers'
41
+ import { WalletError } from '../../src/sdk/WalletError'
42
+ import { StorageSyncReader } from '../../src/storage/StorageSyncReader'
43
+ import { StorageProvider } from '../../src/storage/StorageProvider'
44
+ import { TableProvenTx } from '../../src/storage/schema/tables/TableProvenTx'
45
+ import { TableProvenTxReq } from '../../src/storage/schema/tables/TableProvenTxReq'
46
+ import { TableUser } from '../../src/storage/schema/tables/TableUser'
47
+ import { TableCertificate } from '../../src/storage/schema/tables/TableCertificate'
48
+ import { TableCertificateField } from '../../src/storage/schema/tables/TableCertificateField'
49
+ import { TableOutputBasket } from '../../src/storage/schema/tables/TableOutputBasket'
50
+ import { TableTransaction } from '../../src/storage/schema/tables/TableTransaction'
51
+ import { TableOutput } from '../../src/storage/schema/tables/TableOutput'
52
+ import { TableOutputTag } from '../../src/storage/schema/tables/TableOutputTag'
53
+ import { TableOutputTagMap } from '../../src/storage/schema/tables/TableOutputTagMap'
54
+ import { TableTxLabel } from '../../src/storage/schema/tables/TableTxLabel'
55
+ import { TableTxLabelMap } from '../../src/storage/schema/tables/TableTxLabelMap'
56
+ import { TableSyncState } from '../../src/storage/schema/tables/TableSyncState'
57
+ import { TableMonitorEvent } from '../../src/storage/schema/tables/TableMonitorEvent'
58
+ import { TableCommission } from '../../src/storage/schema/tables/TableCommission'
59
+ import { asArray } from '../../src/utility/utilityHelpers.noBuffer'
60
+ import { ScriptTemplateBRC29 } from '../../src/utility/ScriptTemplateBRC29'
61
+
62
+ dotenv.config()
63
+
64
+ const localMySqlConnection = process.env.MYSQL_CONNECTION || ''
65
+
66
+ export interface TuEnvFlags {
67
+ chain: Chain
68
+ runMySQL: boolean
69
+ runSlowTests: boolean
70
+ logTests: boolean
71
+ }
72
+
73
+ export interface TuEnv extends TuEnvFlags {
74
+ chain: Chain
75
+ identityKey: string
76
+ identityKey2: string
77
+ taalApiKey: string
78
+ bitailsApiKey: string
79
+ whatsonchainApiKey: string
80
+ commissionsIdentity: string
81
+ devKeys: Record<string, string>
82
+ /**
83
+ * file path to local sqlite file for identityKey
84
+ */
85
+ filePath?: string
86
+ /**
87
+ * identityKey for automated test wallet on this chain
88
+ */
89
+ testIdentityKey?: string
90
+ /**
91
+ * file path to local sqlite file for testIdentityKey
92
+ */
93
+ testFilePath?: string
94
+ cloudMySQLConnection?: string
95
+ }
96
+
97
+ export abstract class TestUtilsWalletStorage {
98
+ /**
99
+ * @param chain
100
+ * @returns true if .env has truthy idenityKey, idenityKey2 values for chain
101
+ */
102
+ static noEnv(chain: Chain): boolean {
103
+ try {
104
+ Setup.getEnv(chain)
105
+ return false
106
+ } catch {
107
+ return true
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @param chain
113
+ * @returns true if .env is not valid for chain or testIdentityKey or testFilePath are undefined or empty.
114
+ */
115
+ static noTestEnv(chain: Chain): boolean {
116
+ try {
117
+ const env = _tu.getEnv(chain)
118
+ return !env.testIdentityKey || !env.testFilePath
119
+ } catch {
120
+ return true
121
+ }
122
+ }
123
+
124
+ static getEnvFlags(chain: Chain): TuEnvFlags {
125
+ const logTests = !!process.env.LOGTESTS
126
+ const runMySQL = !!process.env.RUNMYSQL
127
+ const runSlowTests = !!process.env.RUNSLOWTESTS
128
+ return {
129
+ chain,
130
+ runMySQL,
131
+ runSlowTests,
132
+ logTests
133
+ }
134
+ }
135
+
136
+ static getEnv(chain: Chain): TuEnv {
137
+ const flagsEnv = _tu.getEnvFlags(chain)
138
+ // Identity keys of the lead maintainer of this repo...
139
+ const identityKey = (chain === 'main' ? process.env.MY_MAIN_IDENTITY : process.env.MY_TEST_IDENTITY) || ''
140
+ const filePath = chain === 'main' ? process.env.MY_MAIN_FILEPATH : process.env.MY_TEST_FILEPATH
141
+ const identityKey2 = (chain === 'main' ? process.env.MY_MAIN_IDENTITY2 : process.env.MY_TEST_IDENTITY2) || ''
142
+ const testIdentityKey = chain === 'main' ? process.env.TEST_MAIN_IDENTITY : process.env.TEST_TEST_IDENTITY
143
+ const testFilePath = chain === 'main' ? process.env.TEST_MAIN_FILEPATH : process.env.TEST_TEST_FILEPATH
144
+ const cloudMySQLConnection =
145
+ chain === 'main' ? process.env.MAIN_CLOUD_MYSQL_CONNECTION : process.env.TEST_CLOUD_MYSQL_CONNECTION
146
+ const DEV_KEYS = process.env.DEV_KEYS || '{}'
147
+ const taalApiKey = (chain === 'main' ? process.env.MAIN_TAAL_API_KEY : process.env.TEST_TAAL_API_KEY) || ''
148
+ const bitailsApiKey = (chain === 'main' ? process.env.MAIN_BITAILS_API_KEY : process.env.TEST_BITAILS_API_KEY) || ''
149
+ const whatsonchainApiKey =
150
+ (chain === 'main' ? process.env.MAIN_WHATSONCHAIN_API_KEY : process.env.TEST_WHATSONCHAIN_API_KEY) || ''
151
+ const commissionsIdentity =
152
+ (chain === 'main' ? process.env.MAIN_COMMISSIONS_IDENTITY : process.env.TEST_COMMISSIONS_IDENTITY) || ''
153
+ return {
154
+ ...flagsEnv,
155
+ identityKey,
156
+ identityKey2,
157
+ taalApiKey,
158
+ bitailsApiKey,
159
+ whatsonchainApiKey,
160
+ commissionsIdentity,
161
+ devKeys: JSON.parse(DEV_KEYS) as Record<string, string>,
162
+ filePath,
163
+ testIdentityKey,
164
+ testFilePath,
165
+ cloudMySQLConnection
166
+ }
167
+ }
168
+
169
+ static async createMainReviewSetup(): Promise<{
170
+ env: TuEnv
171
+ storage: StorageKnex
172
+ services: Services
173
+ }> {
174
+ const env = _tu.getEnv('main')
175
+ if (!env.cloudMySQLConnection) throw new WERR_INVALID_PARAMETER('env.cloudMySQLConnection', 'valid')
176
+ const knex = Setup.createMySQLKnex(env.cloudMySQLConnection)
177
+ const storage = new StorageKnex({
178
+ chain: env.chain,
179
+ knex: knex,
180
+ commissionSatoshis: 0,
181
+ commissionPubKeyHex: undefined,
182
+ feeModel: { model: 'sat/kb', value: 1 }
183
+ })
184
+ const servicesOptions = Services.createDefaultOptions(env.chain)
185
+ if (env.whatsonchainApiKey) servicesOptions.whatsOnChainApiKey = env.whatsonchainApiKey
186
+ const services = new Services(servicesOptions)
187
+ storage.setServices(services)
188
+ await storage.makeAvailable()
189
+ return { env, storage, services }
190
+ }
191
+
192
+ static async createNoSendP2PKHTestOutpoint(
193
+ address: string,
194
+ satoshis: number,
195
+ noSendChange: string[] | undefined,
196
+ wallet: WalletInterface
197
+ ): Promise<{
198
+ noSendChange: string[]
199
+ txid: string
200
+ cr: CreateActionResult
201
+ sr: SignActionResult
202
+ }> {
203
+ return await _tu.createNoSendP2PKHTestOutpoints(1, address, satoshis, noSendChange, wallet)
204
+ }
205
+
206
+ static async createNoSendP2PKHTestOutpoints(
207
+ count: number,
208
+ address: string,
209
+ satoshis: number,
210
+ noSendChange: string[] | undefined,
211
+ wallet: WalletInterface
212
+ ): Promise<{
213
+ noSendChange: string[]
214
+ txid: string
215
+ cr: CreateActionResult
216
+ sr: SignActionResult
217
+ }> {
218
+ const outputs: CreateActionOutput[] = []
219
+ for (let i = 0; i < count; i++) {
220
+ outputs.push({
221
+ basket: `test-p2pkh-output-${i}`,
222
+ satoshis,
223
+ lockingScript: _tu.getLockP2PKH(address).toHex(),
224
+ outputDescription: `p2pkh ${i}`
225
+ })
226
+ }
227
+
228
+ const createArgs: CreateActionArgs = {
229
+ description: `to ${address}`,
230
+ outputs,
231
+ options: {
232
+ noSendChange,
233
+ randomizeOutputs: false,
234
+ signAndProcess: false,
235
+ noSend: true
236
+ }
237
+ }
238
+
239
+ const cr = await wallet.createAction(createArgs)
240
+ noSendChange = cr.noSendChange
241
+
242
+ expect(cr.noSendChange).toBeTruthy()
243
+ expect(cr.sendWithResults).toBeUndefined()
244
+ expect(cr.tx).toBeUndefined()
245
+ expect(cr.txid).toBeUndefined()
246
+
247
+ expect(cr.signableTransaction).toBeTruthy()
248
+ const st = cr.signableTransaction!
249
+ expect(st.reference).toBeTruthy()
250
+ // const tx = Transaction.fromAtomicBEEF(st.tx) // Transaction doesn't support V2 Beef yet.
251
+ const atomicBeef = Beef.fromBinary(st.tx)
252
+ const tx = atomicBeef.txs[atomicBeef.txs.length - 1].tx!
253
+ for (const input of tx.inputs) {
254
+ expect(atomicBeef.findTxid(input.sourceTXID!)).toBeTruthy()
255
+ }
256
+
257
+ // Spending authorization check happens here...
258
+ //expect(st.amount > 242 && st.amount < 300).toBe(true)
259
+ // sign and complete
260
+ const signArgs: SignActionArgs = {
261
+ reference: st.reference,
262
+ spends: {},
263
+ options: {
264
+ returnTXIDOnly: true,
265
+ noSend: true
266
+ }
267
+ }
268
+
269
+ const sr = await wallet.signAction(signArgs)
270
+
271
+ let txid = sr.txid!
272
+ // Update the noSendChange txid to final signed value.
273
+ noSendChange = noSendChange!.map(op => `${txid}.${op.split('.')[1]}`)
274
+ return { noSendChange, txid, cr, sr }
275
+ }
276
+
277
+ static getKeyPair(priv?: string | PrivateKey): TestKeyPair {
278
+ if (priv === undefined) priv = PrivateKey.fromRandom()
279
+ else if (typeof priv === 'string') priv = new PrivateKey(priv, 'hex')
280
+
281
+ const pub = PublicKey.fromPrivateKey(priv)
282
+ const address = pub.toAddress()
283
+ return { privateKey: priv, publicKey: pub, address }
284
+ }
285
+
286
+ static getLockP2PKH(address: string) {
287
+ const p2pkh = new P2PKH()
288
+ const lock = p2pkh.lock(address)
289
+ return lock
290
+ }
291
+
292
+ static getUnlockP2PKH(priv: PrivateKey, satoshis: number) {
293
+ const p2pkh = new P2PKH()
294
+ const lock = _tu.getLockP2PKH(_tu.getKeyPair(priv).address)
295
+ // Prepare to pay with SIGHASH_ALL and without ANYONE_CAN_PAY.
296
+ // In otherwords:
297
+ // - all outputs must remain in the current order, amount and locking scripts.
298
+ // - all inputs must remain from the current outpoints and sequence numbers.
299
+ // (unlock scripts are never signed)
300
+ const unlock = p2pkh.unlock(priv, 'all', false, satoshis, lock)
301
+ return unlock
302
+ }
303
+
304
+ static async createWalletOnly(args: {
305
+ chain?: Chain
306
+ rootKeyHex?: string
307
+ active?: WalletStorageProvider
308
+ backups?: WalletStorageProvider[]
309
+ privKeyHex?: string
310
+ }): Promise<TestWalletOnly> {
311
+ args.chain ||= 'test'
312
+ args.rootKeyHex ||= '1'.repeat(64)
313
+ const rootKey = PrivateKey.fromHex(args.rootKeyHex)
314
+ const identityKey = rootKey.toPublicKey().toString()
315
+ const keyDeriver = new CachedKeyDeriver(rootKey)
316
+ const chain = args.chain
317
+ const storage = new WalletStorageManager(identityKey, args.active, args.backups)
318
+ if (storage.canMakeAvailable()) await storage.makeAvailable()
319
+ const env = _tu.getEnv(args.chain!)
320
+ const serviceOptions: WalletServicesOptions = Services.createDefaultOptions(env.chain)
321
+ serviceOptions.taalApiKey = env.taalApiKey
322
+ serviceOptions.arcConfig.apiKey = env.taalApiKey
323
+ serviceOptions.bitailsApiKey = env.bitailsApiKey
324
+ serviceOptions.whatsOnChainApiKey = env.whatsonchainApiKey
325
+ const services = new Services(serviceOptions)
326
+ const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services)
327
+ const monitor = new Monitor(monopts)
328
+ monitor.addDefaultTasks()
329
+ let privilegedKeyManager: PrivilegedKeyManager | undefined = undefined
330
+ if (args.privKeyHex) {
331
+ const privKey = PrivateKey.fromString(args.privKeyHex)
332
+ privilegedKeyManager = new PrivilegedKeyManager(async () => privKey)
333
+ }
334
+ const wallet = new Wallet({
335
+ chain,
336
+ keyDeriver,
337
+ storage,
338
+ services,
339
+ monitor,
340
+ privilegedKeyManager
341
+ })
342
+ const r: TestWalletOnly = {
343
+ rootKey,
344
+ identityKey,
345
+ keyDeriver,
346
+ chain,
347
+ storage,
348
+ services,
349
+ monitor,
350
+ wallet
351
+ }
352
+ return r
353
+ }
354
+
355
+ /**
356
+ * Creates a wallet with both local sqlite and cloud stores, the local store is left active.
357
+ *
358
+ * Requires a valid .env file with chain matching testIdentityKey and testFilePath properties valid.
359
+ * Or `args` with those properties.
360
+ *
361
+ * Verifies wallet has at least 1000 satoshis in at least 10 change utxos.
362
+ *
363
+ * @param chain
364
+ *
365
+ * @returns {TestWalletNoSetup}
366
+ */
367
+ static async createTestWallet(args: Chain | CreateTestWalletArgs): Promise<TestWalletNoSetup> {
368
+ let chain: Chain
369
+ let rootKeyHex: string
370
+ let filePath: string
371
+ let addLocalBackup = false
372
+ let setActiveClient = false
373
+ let useMySQLConnectionForClient = false
374
+ if (typeof args === 'string') {
375
+ chain = args
376
+ const env = _tu.getEnv(chain)
377
+ if (!env.testIdentityKey || !env.testFilePath) {
378
+ throw new WERR_INVALID_PARAMETER('env.testIdentityKey and env.testFilePath', 'valid')
379
+ }
380
+ rootKeyHex = env.devKeys[env.testIdentityKey!]
381
+ filePath = env.testFilePath
382
+ } else {
383
+ chain = args.chain
384
+ rootKeyHex = args.rootKeyHex
385
+ filePath = args.filePath
386
+ addLocalBackup = args.addLocalBackup === true
387
+ setActiveClient = args.setActiveClient === true
388
+ useMySQLConnectionForClient = args.useMySQLConnectionForClient === true
389
+ }
390
+
391
+ const databaseName = path.parse(filePath).name
392
+ const setup = await _tu.createSQLiteTestWallet({
393
+ filePath,
394
+ rootKeyHex,
395
+ databaseName,
396
+ chain
397
+ })
398
+ setup.localStorageIdentityKey = setup.storage.getActiveStore()
399
+
400
+ let client: WalletStorageProvider
401
+ if (useMySQLConnectionForClient) {
402
+ const env = _tu.getEnv(chain)
403
+ if (!env.cloudMySQLConnection) throw new WERR_INVALID_PARAMETER('env.cloundMySQLConnection', 'valid')
404
+ const connection = JSON.parse(env.cloudMySQLConnection)
405
+ client = new StorageKnex({
406
+ ...StorageKnex.defaultOptions(),
407
+ knex: _tu.createMySQLFromConnection(connection),
408
+ chain: env.chain
409
+ })
410
+ } else {
411
+ const endpointUrl =
412
+ chain === 'main' ? 'https://storage.babbage.systems' : 'https://staging-storage.babbage.systems'
413
+
414
+ client = new StorageClient(setup.wallet, endpointUrl)
415
+ }
416
+ setup.clientStorageIdentityKey = (await client.makeAvailable()).storageIdentityKey
417
+ await setup.wallet.storage.addWalletStorageProvider(client)
418
+
419
+ if (addLocalBackup) {
420
+ const backupName = `${databaseName}_backup`
421
+ const backupPath = filePath.replace(databaseName, backupName)
422
+ const localBackup = new StorageKnex({
423
+ ...StorageKnex.defaultOptions(),
424
+ knex: _tu.createLocalSQLite(backupPath),
425
+ chain
426
+ })
427
+ await localBackup.migrate(backupName, randomBytesHex(33))
428
+ setup.localBackupStorageIdentityKey = (await localBackup.makeAvailable()).storageIdentityKey
429
+ await setup.wallet.storage.addWalletStorageProvider(localBackup)
430
+ }
431
+
432
+ // SETTING ACTIVE
433
+ // SETTING ACTIVE
434
+ // SETTING ACTIVE
435
+ const log = await setup.storage.setActive(
436
+ setActiveClient ? setup.clientStorageIdentityKey : setup.localStorageIdentityKey
437
+ )
438
+ logger(log)
439
+
440
+ let needsBackup = false
441
+
442
+ if (setup.storage.getActiveStore() === setup.localStorageIdentityKey) {
443
+ const basket = verifyOne(
444
+ await setup.activeStorage.findOutputBaskets({
445
+ partial: {
446
+ userId: setup.storage.getActiveUser().userId,
447
+ name: 'default'
448
+ }
449
+ })
450
+ )
451
+ if (basket.minimumDesiredUTXOValue !== 5 || basket.numberOfDesiredUTXOs < 32) {
452
+ needsBackup = true
453
+ await setup.activeStorage.updateOutputBasket(basket.basketId, {
454
+ minimumDesiredUTXOValue: 5,
455
+ numberOfDesiredUTXOs: 32
456
+ })
457
+ }
458
+ }
459
+
460
+ const balance = await setup.wallet.balanceAndUtxos()
461
+
462
+ if (balance.total < 1000) {
463
+ throw new WERR_INSUFFICIENT_FUNDS(1000, 1000 - balance.total)
464
+ }
465
+
466
+ if (balance.utxos.length <= 10) {
467
+ const args: CreateActionArgs = {
468
+ description: 'spread change'
469
+ }
470
+ await setup.wallet.createAction(args)
471
+ needsBackup = true
472
+ }
473
+
474
+ if (needsBackup) {
475
+ const log2 = await setup.storage.updateBackups()
476
+ console.log(log2)
477
+ }
478
+
479
+ return setup
480
+ }
481
+
482
+ static async createTestWalletWithStorageClient(args: {
483
+ rootKeyHex?: string
484
+ endpointUrl?: string
485
+ chain?: Chain
486
+ }): Promise<TestWalletOnly> {
487
+ args.chain ||= 'test'
488
+ const wo = await _tu.createWalletOnly({
489
+ chain: args.chain,
490
+ rootKeyHex: args.rootKeyHex
491
+ })
492
+ args.endpointUrl ||=
493
+ args.chain === 'main' ? 'https://storage.babbage.systems' : 'https://staging-storage.babbage.systems'
494
+
495
+ const client = new StorageClient(wo.wallet, args.endpointUrl)
496
+ await wo.storage.addWalletStorageProvider(client)
497
+ return wo
498
+ }
499
+
500
+ static async createKnexTestWalletWithSetup<T>(args: {
501
+ knex: Knex<any, any[]>
502
+ databaseName: string
503
+ chain?: Chain
504
+ rootKeyHex?: string
505
+ dropAll?: boolean
506
+ privKeyHex?: string
507
+ insertSetup: (storage: StorageKnex, identityKey: string) => Promise<T>
508
+ }): Promise<TestWallet<T>> {
509
+ const wo = await _tu.createWalletOnly({
510
+ chain: args.chain,
511
+ rootKeyHex: args.rootKeyHex,
512
+ privKeyHex: args.privKeyHex
513
+ })
514
+ const activeStorage = new StorageKnex({
515
+ chain: wo.chain,
516
+ knex: args.knex,
517
+ commissionSatoshis: 0,
518
+ commissionPubKeyHex: undefined,
519
+ feeModel: { model: 'sat/kb', value: 1 }
520
+ })
521
+ if (args.dropAll) await activeStorage.dropAllData()
522
+ await activeStorage.migrate(args.databaseName, randomBytesHex(33))
523
+ await activeStorage.makeAvailable()
524
+ const setup = await args.insertSetup(activeStorage, wo.identityKey)
525
+ await wo.storage.addWalletStorageProvider(activeStorage)
526
+ const { user, isNew } = await activeStorage.findOrInsertUser(wo.identityKey)
527
+ const userId = user.userId
528
+ const r: TestWallet<T> = {
529
+ ...wo,
530
+ activeStorage,
531
+ setup,
532
+ userId
533
+ }
534
+ return r
535
+ }
536
+
537
+ /**
538
+ * Returns path to temporary file in project's './test/data/tmp/' folder.
539
+ *
540
+ * Creates any missing folders.
541
+ *
542
+ * Optionally tries to delete any existing file. This may fail if the file file is locked
543
+ * by another process.
544
+ *
545
+ * Optionally copies filename (or if filename has no dir component, a file of the same filename from the project's './test/data' folder) to initialize file's contents.
546
+ *
547
+ * CAUTION: returned file path will include four random hex digits unless tryToDelete is true. Files must be purged periodically.
548
+ *
549
+ * @param filename target filename without path, optionally just extension in which case random name is used
550
+ * @param tryToDelete true to attempt to delete an existing file at the returned file path.
551
+ * @param copyToTmp true to copy file of same filename from './test/data' (or elsewhere if filename has path) to tmp folder
552
+ * @param reuseExisting true to use existing file if found, otherwise a random string is added to filename.
553
+ * @returns path in './test/data/tmp' folder.
554
+ */
555
+ static async newTmpFile(
556
+ filename = '',
557
+ tryToDelete = false,
558
+ copyToTmp = false,
559
+ reuseExisting = false
560
+ ): Promise<string> {
561
+ const tmpFolder = './test/data/tmp/'
562
+ const p = path.parse(filename)
563
+ const dstDir = tmpFolder
564
+ const dstName = `${p.name}${tryToDelete || reuseExisting ? '' : randomBytesHex(6)}`
565
+ const dstExt = p.ext || 'tmp'
566
+ const dstPath = path.resolve(`${dstDir}${dstName}${dstExt}`)
567
+ await fsp.mkdir(tmpFolder, { recursive: true })
568
+ if (!reuseExisting && (tryToDelete || copyToTmp))
569
+ try {
570
+ await fsp.unlink(dstPath)
571
+ } catch (eu: unknown) {
572
+ const e = WalletError.fromUnknown(eu)
573
+ if (e.name !== 'ENOENT') {
574
+ throw e
575
+ }
576
+ }
577
+ if (copyToTmp) {
578
+ const srcPath = p.dir ? path.resolve(filename) : path.resolve(`./test/data/${filename}`)
579
+ await fsp.copyFile(srcPath, dstPath)
580
+ }
581
+ return dstPath
582
+ }
583
+
584
+ static async copyFile(srcPath: string, dstPath: string): Promise<void> {
585
+ await fsp.copyFile(srcPath, dstPath)
586
+ }
587
+
588
+ static async existingDataFile(filename: string): Promise<string> {
589
+ const folder = './test/data/'
590
+ return folder + filename
591
+ }
592
+
593
+ static createLocalSQLite(filename: string): Knex {
594
+ const config: Knex.Config = {
595
+ client: 'sqlite3',
596
+ connection: { filename },
597
+ useNullAsDefault: true
598
+ }
599
+ const knex = makeKnex(config)
600
+ return knex
601
+ }
602
+
603
+ static createMySQLFromConnection(connection: object): Knex {
604
+ const config: Knex.Config = {
605
+ client: 'mysql2',
606
+ connection,
607
+ useNullAsDefault: true,
608
+ pool: { min: 0, max: 7, idleTimeoutMillis: 15000 }
609
+ }
610
+ const knex = makeKnex(config)
611
+ return knex
612
+ }
613
+
614
+ static createLocalMySQL(database: string): Knex {
615
+ const connection = JSON.parse(localMySqlConnection || '{}')
616
+ connection['database'] = database
617
+ const config: Knex.Config = {
618
+ client: 'mysql2',
619
+ connection,
620
+ useNullAsDefault: true,
621
+ pool: { min: 0, max: 7, idleTimeoutMillis: 15000 }
622
+ }
623
+ const knex = makeKnex(config)
624
+ return knex
625
+ }
626
+
627
+ static async createMySQLTestWallet(args: {
628
+ databaseName: string
629
+ chain?: Chain
630
+ rootKeyHex?: string
631
+ dropAll?: boolean
632
+ }): Promise<TestWallet<{}>> {
633
+ return await this.createKnexTestWallet({
634
+ ...args,
635
+ knex: _tu.createLocalMySQL(args.databaseName)
636
+ })
637
+ }
638
+
639
+ static async createMySQLTestSetup1Wallet(args: {
640
+ databaseName: string
641
+ chain?: Chain
642
+ rootKeyHex?: string
643
+ }): Promise<TestWallet<TestSetup1>> {
644
+ return await this.createKnexTestSetup1Wallet({
645
+ ...args,
646
+ dropAll: true,
647
+ knex: _tu.createLocalMySQL(args.databaseName)
648
+ })
649
+ }
650
+
651
+ static async createMySQLTestSetup2Wallet(args: {
652
+ databaseName: string
653
+ mockData: MockData
654
+ chain?: Chain
655
+ rootKeyHex?: string
656
+ }): Promise<TestWallet<TestSetup2>> {
657
+ return await this.createKnexTestSetup2Wallet({
658
+ ...args,
659
+ dropAll: true,
660
+ knex: _tu.createLocalMySQL(args.databaseName)
661
+ })
662
+ }
663
+
664
+ static async createSQLiteTestWallet(args: {
665
+ filePath?: string
666
+ databaseName: string
667
+ chain?: Chain
668
+ rootKeyHex?: string
669
+ dropAll?: boolean
670
+ privKeyHex?: string
671
+ }): Promise<TestWalletNoSetup> {
672
+ const localSQLiteFile = args.filePath || (await _tu.newTmpFile(`${args.databaseName}.sqlite`, false, false, true))
673
+ return await this.createKnexTestWallet({
674
+ ...args,
675
+ knex: _tu.createLocalSQLite(localSQLiteFile)
676
+ })
677
+ }
678
+
679
+ static async createSQLiteTestSetup1Wallet(args: {
680
+ databaseName: string
681
+ chain?: Chain
682
+ rootKeyHex?: string
683
+ }): Promise<TestWallet<TestSetup1>> {
684
+ const localSQLiteFile = await _tu.newTmpFile(`${args.databaseName}.sqlite`, false, false, true)
685
+ return await this.createKnexTestSetup1Wallet({
686
+ ...args,
687
+ dropAll: true,
688
+ knex: _tu.createLocalSQLite(localSQLiteFile)
689
+ })
690
+ }
691
+
692
+ static async createSQLiteTestSetup2Wallet(args: {
693
+ databaseName: string
694
+ mockData: MockData
695
+ chain?: Chain
696
+ rootKeyHex?: string
697
+ }): Promise<TestWallet<TestSetup2>> {
698
+ const localSQLiteFile = await _tu.newTmpFile(`${args.databaseName}.sqlite`, false, false, true)
699
+ return await this.createKnexTestSetup2Wallet({
700
+ ...args,
701
+ dropAll: true,
702
+ knex: _tu.createLocalSQLite(localSQLiteFile)
703
+ })
704
+ }
705
+
706
+ static async createKnexTestWallet(args: {
707
+ knex: Knex<any, any[]>
708
+ databaseName: string
709
+ chain?: Chain
710
+ rootKeyHex?: string
711
+ dropAll?: boolean
712
+ privKeyHex?: string
713
+ }): Promise<TestWalletNoSetup> {
714
+ return await _tu.createKnexTestWalletWithSetup({
715
+ ...args,
716
+ insertSetup: insertEmptySetup
717
+ })
718
+ }
719
+
720
+ static async createKnexTestSetup1Wallet(args: {
721
+ knex: Knex<any, any[]>
722
+ databaseName: string
723
+ chain?: Chain
724
+ rootKeyHex?: string
725
+ dropAll?: boolean
726
+ }): Promise<TestWallet<TestSetup1>> {
727
+ return await _tu.createKnexTestWalletWithSetup({
728
+ ...args,
729
+ insertSetup: _tu.createTestSetup1
730
+ })
731
+ }
732
+
733
+ static async createKnexTestSetup2Wallet(args: {
734
+ knex: Knex<any, any[]>
735
+ databaseName: string
736
+ mockData: MockData
737
+ chain?: Chain
738
+ rootKeyHex?: string
739
+ dropAll?: boolean
740
+ }): Promise<TestWallet<TestSetup2>> {
741
+ return await _tu.createKnexTestWalletWithSetup({
742
+ ...args,
743
+ insertSetup: async (storage: StorageKnex, identityKey: string) => {
744
+ return _tu.createTestSetup2(storage, identityKey, args.mockData)
745
+ }
746
+ })
747
+ }
748
+
749
+ static async fileExists(file: string): Promise<boolean> {
750
+ try {
751
+ const f = await fsp.open(file, 'r')
752
+ await f.close()
753
+ return true
754
+ } catch (eu: unknown) {
755
+ return false
756
+ }
757
+ }
758
+
759
+ //if (await _tu.fileExists(walletFile))
760
+ static async createLegacyWalletSQLiteCopy(databaseName: string): Promise<TestWalletNoSetup> {
761
+ const walletFile = await _tu.newTmpFile(`${databaseName}.sqlite`, false, false, true)
762
+ const walletKnex = _tu.createLocalSQLite(walletFile)
763
+ return await _tu.createLegacyWalletCopy(databaseName, walletKnex, walletFile)
764
+ }
765
+
766
+ static async createLegacyWalletMySQLCopy(databaseName: string): Promise<TestWalletNoSetup> {
767
+ const walletKnex = _tu.createLocalMySQL(databaseName)
768
+ return await _tu.createLegacyWalletCopy(databaseName, walletKnex)
769
+ }
770
+
771
+ static async createLiveWalletSQLiteWARNING(
772
+ databaseFullPath: string = './test/data/walletLiveTestData.sqlite'
773
+ ): Promise<TestWalletNoSetup> {
774
+ return await this.createKnexTestWallet({
775
+ chain: 'test',
776
+ rootKeyHex: _tu.legacyRootKeyHex,
777
+ databaseName: 'walletLiveTestData',
778
+ knex: _tu.createLocalSQLite(databaseFullPath)
779
+ })
780
+ }
781
+
782
+ static async createWalletSQLite(
783
+ databaseFullPath: string = './test/data/tmp/walletNewTestData.sqlite',
784
+ databaseName: string = 'walletNewTestData'
785
+ ): Promise<TestWalletNoSetup> {
786
+ return await this.createSQLiteTestWallet({
787
+ filePath: databaseFullPath,
788
+ databaseName,
789
+ chain: 'test',
790
+ rootKeyHex: '1'.repeat(64),
791
+ dropAll: true
792
+ })
793
+ }
794
+
795
+ static legacyRootKeyHex = '153a3df216' + '686f55b253991c' + '7039da1f648' + 'ffc5bfe93d6ac2c25ac' + '2d4070918d'
796
+
797
+ static async createLegacyWalletCopy(
798
+ databaseName: string,
799
+ walletKnex: Knex<any, any[]>,
800
+ tryCopyToPath?: string
801
+ ): Promise<TestWalletNoSetup> {
802
+ const readerFile = await _tu.existingDataFile(`walletLegacyTestData.sqlite`)
803
+ let useReader = true
804
+ if (tryCopyToPath) {
805
+ await _tu.copyFile(readerFile, tryCopyToPath)
806
+ //console.log('USING FILE COPY INSTEAD OF SOURCE DB SYNC')
807
+ useReader = false
808
+ }
809
+ const chain: Chain = 'test'
810
+ const rootKeyHex = _tu.legacyRootKeyHex
811
+ const identityKey = '03ac2d10bdb0023f4145cc2eba2fcd2ad3070cb2107b0b48170c46a9440e4cc3fe'
812
+ const rootKey = PrivateKey.fromHex(rootKeyHex)
813
+ const keyDeriver = new CachedKeyDeriver(rootKey)
814
+ const activeStorage = new StorageKnex({
815
+ chain,
816
+ knex: walletKnex,
817
+ commissionSatoshis: 0,
818
+ commissionPubKeyHex: undefined,
819
+ feeModel: { model: 'sat/kb', value: 1 }
820
+ })
821
+ if (useReader) await activeStorage.dropAllData()
822
+ await activeStorage.migrate(databaseName, randomBytesHex(33))
823
+ await activeStorage.makeAvailable()
824
+ const storage = new WalletStorageManager(identityKey, activeStorage)
825
+ await storage.makeAvailable()
826
+ if (useReader) {
827
+ const readerKnex = _tu.createLocalSQLite(readerFile)
828
+ const reader = new StorageKnex({
829
+ chain,
830
+ knex: readerKnex,
831
+ commissionSatoshis: 0,
832
+ commissionPubKeyHex: undefined,
833
+ feeModel: { model: 'sat/kb', value: 1 }
834
+ })
835
+ await reader.makeAvailable()
836
+ await storage.syncFromReader(identityKey, new StorageSyncReader({ identityKey }, reader))
837
+ await reader.destroy()
838
+ }
839
+ const services = new Services(chain)
840
+ const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services)
841
+ const monitor = new Monitor(monopts)
842
+ const wallet = new Wallet({ chain, keyDeriver, storage, services, monitor })
843
+ const userId = verifyTruthy(await activeStorage.findUserByIdentityKey(identityKey)).userId
844
+ const r: TestWallet<{}> = {
845
+ rootKey,
846
+ identityKey,
847
+ keyDeriver,
848
+ chain,
849
+ activeStorage,
850
+ storage,
851
+ setup: {},
852
+ services,
853
+ monitor,
854
+ wallet,
855
+ userId
856
+ }
857
+ return r
858
+ }
859
+
860
+ static wrapProfiling(o: Object, name: string): Record<string, { count: number; totalMsecs: number }> {
861
+ const getFunctionsNames = (obj: Object) => {
862
+ let fNames: string[] = []
863
+ do {
864
+ fNames = fNames.concat(
865
+ Object.getOwnPropertyNames(obj).filter(p => p !== 'constructor' && typeof obj[p] === 'function')
866
+ )
867
+ } while ((obj = Object.getPrototypeOf(obj)) && obj !== Object.prototype)
868
+
869
+ return fNames
870
+ }
871
+
872
+ const notifyPerformance = (fn, performanceDetails) => {
873
+ setTimeout(() => {
874
+ let { functionName, args, startTime, endTime } = performanceDetails
875
+ let _args = args
876
+ if (Array.isArray(args)) {
877
+ _args = args.map(arg => {
878
+ if (typeof arg === 'function') {
879
+ let fName = arg.name
880
+ if (!fName) {
881
+ fName = 'function'
882
+ } else if (fName === 'callbackWrapper') {
883
+ fName = 'callback'
884
+ }
885
+ arg = `[${fName} Function]`
886
+ }
887
+ return arg
888
+ })
889
+ }
890
+ fn({ functionName, args: _args, startTime, endTime })
891
+ }, 0)
892
+ }
893
+
894
+ const stats: Record<string, { count: number; totalMsecs: number }> = {}
895
+
896
+ function logger(args: { functionName: string; args: any; startTime: number; endTime: number }) {
897
+ let s = stats[args.functionName]
898
+ if (!s) {
899
+ s = { count: 0, totalMsecs: 0 }
900
+ stats[args.functionName] = s
901
+ }
902
+ s.count++
903
+ s.totalMsecs += args.endTime - args.startTime
904
+ }
905
+
906
+ const performanceWrapper = (obj: Object, objectName: string, performanceNotificationCallback: any) => {
907
+ let _notifyPerformance = notifyPerformance.bind(null, performanceNotificationCallback)
908
+ let fNames = getFunctionsNames(obj)
909
+ for (let fName of fNames) {
910
+ let originalFunction = obj[fName]
911
+ let wrapperFunction = (...args) => {
912
+ let callbackFnIndex = -1
913
+ let startTime = Date.now()
914
+ let _callBack = args.filter((arg, i) => {
915
+ let _isFunction = typeof arg === 'function'
916
+ if (_isFunction) {
917
+ callbackFnIndex = i
918
+ }
919
+ return _isFunction
920
+ })[0]
921
+ if (_callBack) {
922
+ let callbackWrapper = (...callbackArgs) => {
923
+ let endTime = Date.now()
924
+ _notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
925
+ _callBack.apply(null, callbackArgs)
926
+ }
927
+ args[callbackFnIndex] = callbackWrapper
928
+ }
929
+ let originalReturnObject = originalFunction.apply(obj, args)
930
+ let isPromiseType =
931
+ originalReturnObject &&
932
+ typeof originalReturnObject.then === 'function' &&
933
+ typeof originalReturnObject.catch === 'function'
934
+ if (isPromiseType) {
935
+ return originalReturnObject
936
+ .then(resolveArgs => {
937
+ let endTime = Date.now()
938
+ _notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
939
+ return Promise.resolve(resolveArgs)
940
+ })
941
+ .catch((...rejectArgs) => {
942
+ let endTime = Date.now()
943
+ _notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
944
+ return Promise.reject(...rejectArgs)
945
+ })
946
+ }
947
+ if (!_callBack && !isPromiseType) {
948
+ let endTime = Date.now()
949
+ _notifyPerformance({ functionName: `${objectName}.${fName}`, args, startTime, endTime })
950
+ }
951
+ return originalReturnObject
952
+ }
953
+ obj[fName] = wrapperFunction
954
+ }
955
+
956
+ return obj
957
+ }
958
+
959
+ const functionNames = getFunctionsNames(o)
960
+
961
+ performanceWrapper(o, name, logger)
962
+
963
+ return stats
964
+ }
965
+
966
+ static async createIdbLegacyWalletCopy(databaseName: string): Promise<TestWalletProviderNoSetup> {
967
+ const chain: Chain = 'test'
968
+
969
+ const readerFile = await _tu.existingDataFile(`walletLegacyTestData.sqlite`)
970
+ const readerKnex = _tu.createLocalSQLite(readerFile)
971
+ const reader = new StorageKnex({
972
+ chain,
973
+ knex: readerKnex,
974
+ commissionSatoshis: 0,
975
+ commissionPubKeyHex: undefined,
976
+ feeModel: { model: 'sat/kb', value: 1 }
977
+ })
978
+ await reader.makeAvailable()
979
+
980
+ const rootKeyHex = _tu.legacyRootKeyHex
981
+ const identityKey = '03ac2d10bdb0023f4145cc2eba2fcd2ad3070cb2107b0b48170c46a9440e4cc3fe'
982
+ const rootKey = PrivateKey.fromHex(rootKeyHex)
983
+ const keyDeriver = new CachedKeyDeriver(rootKey)
984
+
985
+ const activeStorage = new StorageIdb({
986
+ chain,
987
+ commissionSatoshis: 0,
988
+ commissionPubKeyHex: undefined,
989
+ feeModel: { model: 'sat/kb', value: 1 }
990
+ })
991
+
992
+ await activeStorage.dropAllData()
993
+ await activeStorage.migrate(databaseName, randomBytesHex(33))
994
+ await activeStorage.makeAvailable()
995
+
996
+ const storage = new WalletStorageManager(identityKey, activeStorage)
997
+ await storage.makeAvailable()
998
+
999
+ await storage.syncFromReader(identityKey, new StorageSyncReader({ identityKey }, reader))
1000
+
1001
+ await reader.destroy()
1002
+
1003
+ const services = new Services(chain)
1004
+ const monopts = Monitor.createDefaultWalletMonitorOptions(chain, storage, services)
1005
+ const monitor = new Monitor(monopts)
1006
+ const wallet = new Wallet({ chain, keyDeriver, storage, services, monitor })
1007
+ const userId = verifyTruthy(await activeStorage.findUserByIdentityKey(identityKey)).userId
1008
+ const r: TestWalletProvider<{}> = {
1009
+ rootKey,
1010
+ identityKey,
1011
+ keyDeriver,
1012
+ chain,
1013
+ activeStorage,
1014
+ storage,
1015
+ setup: {},
1016
+ services,
1017
+ monitor,
1018
+ wallet,
1019
+ userId
1020
+ }
1021
+ return r
1022
+ }
1023
+
1024
+ static makeSampleCert(subject?: string): {
1025
+ cert: WalletCertificate
1026
+ subject: string
1027
+ certifier: PrivateKey
1028
+ } {
1029
+ subject ||= PrivateKey.fromRandom().toPublicKey().toString()
1030
+ const certifier = PrivateKey.fromRandom()
1031
+ const verifier = PrivateKey.fromRandom()
1032
+ const cert: WalletCertificate = {
1033
+ type: Utils.toBase64(new Array(32).fill(1)),
1034
+ serialNumber: Utils.toBase64(new Array(32).fill(2)),
1035
+ revocationOutpoint: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef.1',
1036
+ subject,
1037
+ certifier: certifier.toPublicKey().toString(),
1038
+ fields: {
1039
+ name: 'Alice',
1040
+ email: 'alice@example.com',
1041
+ organization: 'Example Corp'
1042
+ },
1043
+ signature: ''
1044
+ }
1045
+ return { cert, subject, certifier }
1046
+ }
1047
+
1048
+ static async insertTestProvenTx(storage: StorageProvider, txid?: string, trx?: TrxToken) {
1049
+ const now = new Date()
1050
+ const ptx: TableProvenTx = {
1051
+ created_at: now,
1052
+ updated_at: now,
1053
+ provenTxId: 0,
1054
+ txid: txid || randomBytesHex(32),
1055
+ height: 1,
1056
+ index: 0,
1057
+ merklePath: [1, 2, 3, 4, 5, 6, 7, 8],
1058
+ rawTx: [4, 5, 6],
1059
+ blockHash: randomBytesHex(32),
1060
+ merkleRoot: randomBytesHex(32)
1061
+ }
1062
+ await storage.insertProvenTx(ptx, trx)
1063
+ return ptx
1064
+ }
1065
+
1066
+ static async insertTestProvenTxReq(
1067
+ storage: StorageProvider,
1068
+ txid?: string,
1069
+ provenTxId?: number,
1070
+ onlyRequired?: boolean
1071
+ ) {
1072
+ const now = new Date()
1073
+ const ptxreq: TableProvenTxReq = {
1074
+ // Required:
1075
+ created_at: now,
1076
+ updated_at: now,
1077
+ provenTxReqId: 0,
1078
+ txid: txid || randomBytesHex(32),
1079
+ status: 'nosend',
1080
+ attempts: 0,
1081
+ notified: false,
1082
+ history: '{}',
1083
+ notify: '{}',
1084
+ rawTx: [4, 5, 6],
1085
+ // Optional:
1086
+ provenTxId: provenTxId || undefined,
1087
+ batch: onlyRequired ? undefined : randomBytesBase64(10),
1088
+ inputBEEF: onlyRequired ? undefined : [1, 2, 3]
1089
+ }
1090
+ await storage.insertProvenTxReq(ptxreq)
1091
+ return ptxreq
1092
+ }
1093
+
1094
+ static async insertTestUser(storage: StorageProvider, identityKey?: string) {
1095
+ const now = new Date()
1096
+ const e: TableUser = {
1097
+ created_at: now,
1098
+ updated_at: now,
1099
+ userId: 0,
1100
+ identityKey: identityKey || randomBytesHex(33),
1101
+ activeStorage: storage.getSettings().storageIdentityKey
1102
+ }
1103
+ await storage.insertUser(e)
1104
+ return e
1105
+ }
1106
+
1107
+ static async insertTestCertificate(storage: StorageProvider, u?: TableUser) {
1108
+ const now = new Date()
1109
+ u ||= await _tu.insertTestUser(storage)
1110
+ const e: TableCertificate = {
1111
+ created_at: now,
1112
+ updated_at: now,
1113
+ certificateId: 0,
1114
+ userId: u.userId,
1115
+ type: randomBytesBase64(33),
1116
+ serialNumber: randomBytesBase64(33),
1117
+ certifier: randomBytesHex(33),
1118
+ subject: randomBytesHex(33),
1119
+ verifier: undefined,
1120
+ revocationOutpoint: `${randomBytesHex(32)}.999`,
1121
+ signature: randomBytesHex(50),
1122
+ isDeleted: false
1123
+ }
1124
+ await storage.insertCertificate(e)
1125
+ return e
1126
+ }
1127
+
1128
+ static async insertTestCertificateField(storage: StorageProvider, c: TableCertificate, name: string, value: string) {
1129
+ const now = new Date()
1130
+ const e: TableCertificateField = {
1131
+ created_at: now,
1132
+ updated_at: now,
1133
+ certificateId: c.certificateId,
1134
+ userId: c.userId,
1135
+ fieldName: name,
1136
+ fieldValue: value,
1137
+ masterKey: randomBytesBase64(40)
1138
+ }
1139
+ await storage.insertCertificateField(e)
1140
+ return e
1141
+ }
1142
+
1143
+ static async insertTestOutputBasket(
1144
+ storage: StorageProvider,
1145
+ u?: TableUser | number,
1146
+ partial?: Partial<TableOutputBasket>
1147
+ ) {
1148
+ const now = new Date()
1149
+ let user: TableUser
1150
+ if (u === undefined) {
1151
+ user = await _tu.insertTestUser(storage)
1152
+ } else if (typeof u === 'number') {
1153
+ user = verifyOne(await storage.findUsers({ partial: { userId: u } })) as TableUser
1154
+ } else {
1155
+ user = u
1156
+ }
1157
+ const e: TableOutputBasket = {
1158
+ created_at: now,
1159
+ updated_at: now,
1160
+ basketId: 0,
1161
+ userId: user.userId,
1162
+ name: randomBytesHex(6),
1163
+ numberOfDesiredUTXOs: 42,
1164
+ minimumDesiredUTXOValue: 1642,
1165
+ isDeleted: false,
1166
+ ...(partial || {})
1167
+ }
1168
+ await storage.insertOutputBasket(e)
1169
+ return e
1170
+ }
1171
+
1172
+ static async insertTestTransaction(
1173
+ storage: StorageProvider,
1174
+ u?: TableUser,
1175
+ onlyRequired?: boolean,
1176
+ partial?: Partial<TableTransaction>
1177
+ ) {
1178
+ const now = new Date()
1179
+ u ||= await _tu.insertTestUser(storage)
1180
+ const e: TableTransaction = {
1181
+ // Required:
1182
+ created_at: now,
1183
+ updated_at: now,
1184
+ transactionId: 0,
1185
+ userId: u.userId,
1186
+ status: 'nosend',
1187
+ reference: randomBytesBase64(10),
1188
+ isOutgoing: true,
1189
+ satoshis: 9999,
1190
+ description: 'buy me a river',
1191
+ // Optional:
1192
+ version: onlyRequired ? undefined : 0,
1193
+ lockTime: onlyRequired ? undefined : 500000000,
1194
+ txid: onlyRequired ? undefined : randomBytesHex(32),
1195
+ inputBEEF: onlyRequired ? undefined : new Beef().toBinary(),
1196
+ rawTx: onlyRequired ? undefined : [1, 2, 3],
1197
+ ...(partial || {})
1198
+ }
1199
+ await storage.insertTransaction(e)
1200
+ return { tx: e, user: u }
1201
+ }
1202
+
1203
+ static async insertTestOutput(
1204
+ storage: StorageProvider,
1205
+ t: TableTransaction,
1206
+ vout: number,
1207
+ satoshis: number,
1208
+ basket?: TableOutputBasket,
1209
+ requiredOnly?: boolean,
1210
+ partial?: Partial<TableOutput>
1211
+ ) {
1212
+ const now = new Date()
1213
+ const e: TableOutput = {
1214
+ created_at: now,
1215
+ updated_at: now,
1216
+ outputId: 0,
1217
+ userId: t.userId,
1218
+ transactionId: t.transactionId,
1219
+ basketId: basket ? basket.basketId : undefined,
1220
+ spendable: true,
1221
+ change: true,
1222
+ outputDescription: 'not mutch to say',
1223
+ vout,
1224
+ satoshis,
1225
+ providedBy: 'you',
1226
+ purpose: 'secret',
1227
+ type: 'custom',
1228
+ txid: requiredOnly ? undefined : randomBytesHex(32),
1229
+ senderIdentityKey: requiredOnly ? undefined : randomBytesHex(32),
1230
+ derivationPrefix: requiredOnly ? undefined : randomBytesHex(16),
1231
+ derivationSuffix: requiredOnly ? undefined : randomBytesHex(16),
1232
+ spentBy: undefined, // must be a valid transactionId
1233
+ sequenceNumber: requiredOnly ? undefined : 42,
1234
+ spendingDescription: requiredOnly ? undefined : randomBytesHex(16),
1235
+ scriptLength: requiredOnly ? undefined : 36,
1236
+ scriptOffset: requiredOnly ? undefined : 12,
1237
+ lockingScript: requiredOnly ? undefined : asArray(randomBytesHex(36)),
1238
+ ...(partial || {})
1239
+ }
1240
+ await storage.insertOutput(e)
1241
+ return e
1242
+ }
1243
+
1244
+ static async insertTestOutputTag(storage: StorageProvider, u: TableUser, partial?: Partial<TableOutputTag>) {
1245
+ const now = new Date()
1246
+ const e: TableOutputTag = {
1247
+ created_at: now,
1248
+ updated_at: now,
1249
+ outputTagId: 0,
1250
+ userId: u.userId,
1251
+ tag: randomBytesHex(6),
1252
+ isDeleted: false,
1253
+ ...(partial || {})
1254
+ }
1255
+ await storage.insertOutputTag(e)
1256
+ return e
1257
+ }
1258
+
1259
+ static async insertTestOutputTagMap(storage: StorageProvider, o: TableOutput, tag: TableOutputTag) {
1260
+ const now = new Date()
1261
+ const e: TableOutputTagMap = {
1262
+ created_at: now,
1263
+ updated_at: now,
1264
+ outputTagId: tag.outputTagId,
1265
+ outputId: o.outputId,
1266
+ isDeleted: false
1267
+ }
1268
+ await storage.insertOutputTagMap(e)
1269
+ return e
1270
+ }
1271
+
1272
+ static async insertTestTxLabel(storage: StorageProvider, u: TableUser, partial?: Partial<TableTxLabel>) {
1273
+ const now = new Date()
1274
+ const e: TableTxLabel = {
1275
+ created_at: now,
1276
+ updated_at: now,
1277
+ txLabelId: 0,
1278
+ userId: u.userId,
1279
+ label: randomBytesHex(6),
1280
+ isDeleted: false,
1281
+ ...(partial || {})
1282
+ }
1283
+ await storage.insertTxLabel(e)
1284
+ return e
1285
+ }
1286
+
1287
+ static async insertTestTxLabelMap(
1288
+ storage: StorageProvider,
1289
+ tx: TableTransaction,
1290
+ label: TableTxLabel,
1291
+ partial?: Partial<TableTxLabelMap>
1292
+ ) {
1293
+ const now = new Date()
1294
+ const e: TableTxLabelMap = {
1295
+ created_at: now,
1296
+ updated_at: now,
1297
+ txLabelId: label.txLabelId,
1298
+ transactionId: tx.transactionId,
1299
+ isDeleted: false,
1300
+ ...(partial || {})
1301
+ }
1302
+ await storage.insertTxLabelMap(e)
1303
+ return e
1304
+ }
1305
+
1306
+ static async insertTestSyncState(storage: StorageProvider, u: TableUser) {
1307
+ const now = new Date()
1308
+ const settings = await storage.getSettings()
1309
+ const e: TableSyncState = {
1310
+ created_at: now,
1311
+ updated_at: now,
1312
+ syncStateId: 0,
1313
+ userId: u.userId,
1314
+ storageIdentityKey: settings.storageIdentityKey,
1315
+ storageName: settings.storageName,
1316
+ status: 'unknown',
1317
+ init: false,
1318
+ refNum: randomBytesBase64(10),
1319
+ syncMap: '{}'
1320
+ }
1321
+ await storage.insertSyncState(e)
1322
+ return e
1323
+ }
1324
+
1325
+ static async insertTestMonitorEvent(storage: StorageProvider) {
1326
+ const now = new Date()
1327
+ const e: TableMonitorEvent = {
1328
+ created_at: now,
1329
+ updated_at: now,
1330
+ id: 0,
1331
+ event: 'nothing much happened'
1332
+ }
1333
+ await storage.insertMonitorEvent(e)
1334
+ return e
1335
+ }
1336
+
1337
+ static async insertTestCommission(storage: StorageProvider, t: TableTransaction) {
1338
+ const now = new Date()
1339
+ const e: TableCommission = {
1340
+ created_at: now,
1341
+ updated_at: now,
1342
+ commissionId: 0,
1343
+ userId: t.userId,
1344
+ transactionId: t.transactionId,
1345
+ satoshis: 200,
1346
+ keyOffset: randomBytesBase64(32),
1347
+ isRedeemed: false,
1348
+ lockingScript: [1, 2, 3]
1349
+ }
1350
+ await storage.insertCommission(e)
1351
+ return e
1352
+ }
1353
+
1354
+ static async createTestSetup1(storage: StorageProvider, u1IdentityKey?: string): Promise<TestSetup1> {
1355
+ const u1 = await _tu.insertTestUser(storage, u1IdentityKey)
1356
+ const u1basket1 = await _tu.insertTestOutputBasket(storage, u1)
1357
+ const u1basket2 = await _tu.insertTestOutputBasket(storage, u1)
1358
+ const u1label1 = await _tu.insertTestTxLabel(storage, u1)
1359
+ const u1label2 = await _tu.insertTestTxLabel(storage, u1)
1360
+ const u1tag1 = await _tu.insertTestOutputTag(storage, u1)
1361
+ const u1tag2 = await _tu.insertTestOutputTag(storage, u1)
1362
+ const { tx: u1tx1 } = await _tu.insertTestTransaction(storage, u1)
1363
+ const u1comm1 = await _tu.insertTestCommission(storage, u1tx1)
1364
+ const u1tx1label1 = await _tu.insertTestTxLabelMap(storage, u1tx1, u1label1)
1365
+ const u1tx1label2 = await _tu.insertTestTxLabelMap(storage, u1tx1, u1label2)
1366
+ const u1tx1o0 = await _tu.insertTestOutput(storage, u1tx1, 0, 101, u1basket1)
1367
+ const u1o0tag1 = await _tu.insertTestOutputTagMap(storage, u1tx1o0, u1tag1)
1368
+ const u1o0tag2 = await _tu.insertTestOutputTagMap(storage, u1tx1o0, u1tag2)
1369
+ const u1tx1o1 = await _tu.insertTestOutput(storage, u1tx1, 1, 111, u1basket2)
1370
+ const u1o1tag1 = await _tu.insertTestOutputTagMap(storage, u1tx1o1, u1tag1)
1371
+ const u1cert1 = await _tu.insertTestCertificate(storage, u1)
1372
+ const u1cert1field1 = await _tu.insertTestCertificateField(storage, u1cert1, 'bob', 'your uncle')
1373
+ const u1cert1field2 = await _tu.insertTestCertificateField(storage, u1cert1, 'name', 'alice')
1374
+ const u1cert2 = await _tu.insertTestCertificate(storage, u1)
1375
+ const u1cert2field1 = await _tu.insertTestCertificateField(storage, u1cert2, 'name', 'alice')
1376
+ const u1cert3 = await _tu.insertTestCertificate(storage, u1)
1377
+ const u1sync1 = await _tu.insertTestSyncState(storage, u1)
1378
+
1379
+ const u2 = await _tu.insertTestUser(storage)
1380
+ const u2basket1 = await _tu.insertTestOutputBasket(storage, u2)
1381
+ const u2label1 = await _tu.insertTestTxLabel(storage, u2)
1382
+ const { tx: u2tx1 } = await _tu.insertTestTransaction(storage, u2, true)
1383
+ const u2comm1 = await _tu.insertTestCommission(storage, u2tx1)
1384
+ const u2tx1label1 = await _tu.insertTestTxLabelMap(storage, u2tx1, u2label1)
1385
+ const u2tx1o0 = await _tu.insertTestOutput(storage, u2tx1, 0, 101, u2basket1)
1386
+ const { tx: u2tx2 } = await _tu.insertTestTransaction(storage, u2, true)
1387
+ const u2comm2 = await _tu.insertTestCommission(storage, u2tx2)
1388
+
1389
+ const proven1 = await _tu.insertTestProvenTx(storage)
1390
+ const req1 = await _tu.insertTestProvenTxReq(storage, undefined, undefined, true)
1391
+ const req2 = await _tu.insertTestProvenTxReq(storage, proven1.txid, proven1.provenTxId)
1392
+
1393
+ const we1 = await _tu.insertTestMonitorEvent(storage)
1394
+ return {
1395
+ u1,
1396
+ u1basket1,
1397
+ u1basket2,
1398
+ u1label1,
1399
+ u1label2,
1400
+ u1tag1,
1401
+ u1tag2,
1402
+ u1tx1,
1403
+ u1comm1,
1404
+ u1tx1label1,
1405
+ u1tx1label2,
1406
+ u1tx1o0,
1407
+ u1o0tag1,
1408
+ u1o0tag2,
1409
+ u1tx1o1,
1410
+ u1o1tag1,
1411
+ u1cert1,
1412
+ u1cert1field1,
1413
+ u1cert1field2,
1414
+ u1cert2,
1415
+ u1cert2field1,
1416
+ u1cert3,
1417
+ u1sync1,
1418
+
1419
+ u2,
1420
+ u2basket1,
1421
+ u2label1,
1422
+ u2tx1,
1423
+ u2comm1,
1424
+ u2tx1label1,
1425
+ u2tx1o0,
1426
+ u2tx2,
1427
+ u2comm2,
1428
+
1429
+ proven1,
1430
+ req1,
1431
+ req2,
1432
+
1433
+ we1
1434
+ }
1435
+ }
1436
+
1437
+ static async createTestSetup2(
1438
+ storage: StorageProvider,
1439
+ identityKey: string,
1440
+ mockData: MockData = { actions: [] }
1441
+ ): Promise<TestSetup2> {
1442
+ if (!mockData || !mockData.actions) {
1443
+ throw new Error('mockData.actions is required')
1444
+ }
1445
+
1446
+ const now = new Date()
1447
+ const inputTxMap: Record<string, any> = {}
1448
+ const outputMap: Record<string, any> = {}
1449
+
1450
+ // only one user
1451
+ const user = await _tu.insertTestUser(storage, identityKey)
1452
+
1453
+ // First create your output that represent your inputs
1454
+ for (const action of mockData.actions) {
1455
+ for (const input of action.inputs || []) {
1456
+ let prevOutput = outputMap[input.sourceOutpoint]
1457
+
1458
+ if (!prevOutput) {
1459
+ const { tx: transaction } = await _tu.insertTestTransaction(storage, user, false, {
1460
+ txid: input.sourceOutpoint.split('.')[0],
1461
+ satoshis: input.sourceSatoshis,
1462
+ status: 'confirmed' as TransactionStatus,
1463
+ description: 'Generated transaction for input',
1464
+ lockTime: 0,
1465
+ version: 1,
1466
+ inputBEEF: [1, 2, 3, 4],
1467
+ rawTx: [4, 3, 2, 1]
1468
+ })
1469
+
1470
+ const basket = await _tu.insertTestOutputBasket(storage, user, {
1471
+ name: randomBytesHex(6)
1472
+ })
1473
+
1474
+ // Need to convert
1475
+ const lockingScriptValue = input.sourceLockingScript
1476
+ ? Utils.toArray(input.sourceLockingScript, 'hex')
1477
+ : undefined
1478
+
1479
+ prevOutput = await _tu.insertTestOutput(
1480
+ storage,
1481
+ transaction,
1482
+ 0,
1483
+ input.sourceSatoshis,
1484
+ basket,
1485
+ true, // Needs to be spendable
1486
+ {
1487
+ outputDescription: input.inputDescription,
1488
+ spendable: true,
1489
+ vout: Number(input.sourceOutpoint.split('.')[1]),
1490
+ lockingScript: lockingScriptValue,
1491
+ txid: transaction.txid
1492
+ }
1493
+ )
1494
+
1495
+ // Store in maps for later use
1496
+ inputTxMap[input.sourceOutpoint] = transaction
1497
+ outputMap[input.sourceOutpoint] = prevOutput
1498
+ }
1499
+ }
1500
+ }
1501
+
1502
+ // Process transactions that spend those previous outputs
1503
+ for (const action of mockData.actions) {
1504
+ const { tx: transaction } = await _tu.insertTestTransaction(storage, user, false, {
1505
+ txid: `${action.txid}` || `tx_${action.satoshis}_${Date.now()}`,
1506
+ satoshis: action.satoshis,
1507
+ status: action.status as TransactionStatus,
1508
+ description: action.description,
1509
+ lockTime: action.lockTime,
1510
+ version: action.version,
1511
+ inputBEEF: [1, 2, 3, 4],
1512
+ rawTx: [4, 3, 2, 1]
1513
+ })
1514
+
1515
+ // Loop through action inputs and update chosen outputs
1516
+ for (const input of action.inputs || []) {
1517
+ // Output must exist before updating
1518
+ const prevOutput = outputMap[input.sourceOutpoint]
1519
+
1520
+ if (!prevOutput) {
1521
+ throw new Error(`UTXO not found in outputMap for sourceOutpoint: ${input.sourceOutpoint}`)
1522
+ }
1523
+
1524
+ // Set correct output fields as per input fields
1525
+ await storage.updateOutput(prevOutput.outputId, {
1526
+ spendable: false, // Mark output as spent
1527
+ spentBy: transaction.transactionId, // Reference the new transaction
1528
+ spendingDescription: input.inputDescription, // Store description
1529
+ sequenceNumber: input.sequenceNumber // Store sequence number
1530
+ })
1531
+ }
1532
+
1533
+ // Insert any new outputs for the transaction
1534
+ if (action.outputs) {
1535
+ for (const output of action.outputs) {
1536
+ const basket = await _tu.insertTestOutputBasket(storage, user, {
1537
+ name: output.basket
1538
+ })
1539
+ const insertedOutput = await _tu.insertTestOutput(
1540
+ storage,
1541
+ transaction,
1542
+ output.outputIndex,
1543
+ output.satoshis,
1544
+ basket,
1545
+ false,
1546
+ {
1547
+ outputDescription: output.outputDescription,
1548
+ spendable: output.spendable,
1549
+ txid: transaction.txid
1550
+ }
1551
+ )
1552
+
1553
+ // Store this output in the map for future transactions to reference
1554
+ outputMap[`${action.txid}.${output.outputIndex}`] = insertedOutput
1555
+ }
1556
+ }
1557
+
1558
+ // Labels inserted
1559
+ if (action.labels) {
1560
+ for (const label of action.labels) {
1561
+ const l = await _tu.insertTestTxLabel(storage, user, {
1562
+ label,
1563
+ isDeleted: false,
1564
+ created_at: now,
1565
+ updated_at: now,
1566
+ txLabelId: 0,
1567
+ userId: user.userId
1568
+ })
1569
+ await _tu.insertTestTxLabelMap(storage, transaction, l)
1570
+ }
1571
+ }
1572
+
1573
+ // Tags inserted for outputs
1574
+ if (action.outputs) {
1575
+ for (const output of action.outputs) {
1576
+ if (output.tags) {
1577
+ // Ensure we fetch the correct inserted output for the current transaction
1578
+ const insertedOutput = outputMap[`${action.txid}.${output.outputIndex}`]
1579
+
1580
+ if (!insertedOutput) {
1581
+ throw new Error(`Output not found for txid: ${action.txid}, vout: ${output.outputIndex}`)
1582
+ }
1583
+
1584
+ for (const tag of output.tags) {
1585
+ // Insert the output tag into the database
1586
+ const insertedTag = await _tu.insertTestOutputTag(storage, user, {
1587
+ tag: tag,
1588
+ isDeleted: false,
1589
+ created_at: now,
1590
+ updated_at: now,
1591
+ outputTagId: 0, // Will be auto-incremented by the DB
1592
+ userId: user.userId
1593
+ })
1594
+
1595
+ // Map the inserted tag to the correct output
1596
+ await _tu.insertTestOutputTagMap(storage, insertedOutput, insertedTag)
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ return mockData
1604
+ }
1605
+
1606
+ static mockPostServicesAsSuccess(ctxs: TestWalletOnly[]): void {
1607
+ mockPostServices(ctxs, 'success')
1608
+ }
1609
+ static mockPostServicesAsError(ctxs: TestWalletOnly[]): void {
1610
+ mockPostServices(ctxs, 'error')
1611
+ }
1612
+ static mockPostServicesAsCallback(
1613
+ ctxs: TestWalletOnly[],
1614
+ callback: (beef: Beef, txids: string[]) => 'success' | 'error'
1615
+ ): void {
1616
+ mockPostServices(ctxs, 'error', callback)
1617
+ }
1618
+
1619
+ static mockMerklePathServicesAsCallback(
1620
+ ctxs: TestWalletOnly[],
1621
+ callback: (txid: string) => Promise<GetMerklePathResult>
1622
+ ): void {
1623
+ for (const { services } of ctxs) {
1624
+ services.getMerklePath = jest.fn().mockImplementation(async (txid: string): Promise<GetMerklePathResult> => {
1625
+ const r = await callback(txid)
1626
+ return r
1627
+ })
1628
+ }
1629
+ }
1630
+
1631
+ static async createWalletSetupEnv(chain: Chain): Promise<TestWalletOnly> {
1632
+ const env = Setup.getEnv(chain)
1633
+ const rootKeyHex = env.devKeys[env.identityKey]
1634
+
1635
+ if (env.filePath) {
1636
+ return await _tu.createSQLiteTestWallet({
1637
+ filePath: env.filePath,
1638
+ databaseName: 'setupEnvWallet',
1639
+ chain,
1640
+ rootKeyHex
1641
+ })
1642
+ }
1643
+
1644
+ return await _tu.createTestWalletWithStorageClient({
1645
+ chain,
1646
+ rootKeyHex
1647
+ })
1648
+ }
1649
+
1650
+ /**
1651
+ * Create a pair of transacitons that cancel out, other than the transaciton fees.
1652
+ * Both created transactions are left with status 'noSend'.
1653
+ * This allows the transactions to either be broadcast by an external party,
1654
+ * or they may be aborted.
1655
+ *
1656
+ * `doubleSpendTx` should only be used for double spend testing.
1657
+ * It attempts to forward the txidDo input, which should already have been reclaimed by txidUndo, to a random private key output.
1658
+ *
1659
+ * @param wallet the wallet that will create both transactions, or Chain and createWalletEnv is used to create a wallet.
1660
+ * @param satoshis amount of new output created and consumed. Defaults to 41.
1661
+ * @returns { txidDo: string, txidUndo: string, beef: Beef, doubleSpendTx: transaction }
1662
+ */
1663
+ static async createNoSendTxPair(
1664
+ wallet: Wallet | Chain,
1665
+ satoshis = 41
1666
+ ): Promise<{
1667
+ txidDo: string
1668
+ txidUndo: string
1669
+ beef: Beef
1670
+ doubleSpendTx: Transaction
1671
+ }> {
1672
+ let destroyWallet = false
1673
+ if (wallet === 'main' || wallet === 'test') {
1674
+ const setup = await _tu.createWalletSetupEnv(wallet)
1675
+ wallet = setup.wallet
1676
+ if (!setup.storage.isActiveEnabled) await setup.storage.setActive(setup.storage.getActiveStore())
1677
+ destroyWallet = true
1678
+ }
1679
+
1680
+ const derivationPrefix = randomBytesBase64(8)
1681
+ const derivationSuffix = randomBytesBase64(8)
1682
+ const keyDeriver = wallet.keyDeriver
1683
+
1684
+ const t = new ScriptTemplateBRC29({
1685
+ derivationPrefix,
1686
+ derivationSuffix,
1687
+ keyDeriver
1688
+ })
1689
+
1690
+ let label = 'doTxPair'
1691
+ const car = await wallet.createAction({
1692
+ outputs: [
1693
+ {
1694
+ lockingScript: t.lock(keyDeriver.rootKey.toString(), wallet.identityKey).toHex(),
1695
+ satoshis,
1696
+ outputDescription: label,
1697
+ customInstructions: JSON.stringify({
1698
+ derivationPrefix,
1699
+ derivationSuffix,
1700
+ type: 'BRC29'
1701
+ })
1702
+ }
1703
+ ],
1704
+ options: {
1705
+ randomizeOutputs: false,
1706
+ noSend: true
1707
+ },
1708
+ description: label
1709
+ })
1710
+
1711
+ const beef = Beef.fromBinary(car.tx!)
1712
+ const txidDo = car.txid!
1713
+ const outpoint = `${car.txid!}.0`
1714
+
1715
+ const unlock = t.unlock(keyDeriver.rootKey.toString(), wallet.identityKey, satoshis)
1716
+
1717
+ label = 'undoTxPair'
1718
+
1719
+ const car2 = await wallet.createAction({
1720
+ inputBEEF: beef.toBinary(),
1721
+ inputs: [
1722
+ {
1723
+ outpoint,
1724
+ unlockingScriptLength: t.unlockLength,
1725
+ inputDescription: label
1726
+ }
1727
+ ],
1728
+ description: label,
1729
+ options: {
1730
+ noSend: true,
1731
+ noSendChange: car.noSendChange
1732
+ }
1733
+ })
1734
+
1735
+ const st = car2.signableTransaction!
1736
+ const stBeef = Beef.fromBinary(st.tx)
1737
+ const tx = stBeef.findAtomicTransaction(stBeef.txs.slice(-1)[0].txid)!
1738
+ tx.inputs[0].unlockingScriptTemplate = unlock
1739
+ await tx.sign()
1740
+ const unlockingScript = tx.inputs[0].unlockingScript!.toHex()
1741
+
1742
+ const signArgs: SignActionArgs = {
1743
+ reference: st.reference,
1744
+ spends: { 0: { unlockingScript } },
1745
+ options: {
1746
+ noSend: true
1747
+ }
1748
+ }
1749
+
1750
+ const sar = await wallet.signAction(signArgs)
1751
+
1752
+ beef.mergeBeef(sar.tx!)
1753
+ const txidUndo = sar.txid!
1754
+
1755
+ if (destroyWallet) await wallet.destroy()
1756
+
1757
+ const doubleSpendTx = new Transaction()
1758
+ const sourceTXID = txidDo
1759
+ const sourceOutputIndex = 0
1760
+ const sourceSatoshis = satoshis
1761
+ doubleSpendTx.addInput({
1762
+ sourceOutputIndex,
1763
+ sourceTXID,
1764
+ sourceTransaction: beef.findAtomicTransaction(sourceTXID),
1765
+ unlockingScriptTemplate: unlock
1766
+ })
1767
+ doubleSpendTx.addOutput({
1768
+ satoshis: sourceSatoshis - 10,
1769
+ lockingScript: new P2PKH().lock(PrivateKey.fromRandom().toAddress())
1770
+ })
1771
+ await doubleSpendTx.sign()
1772
+
1773
+ return {
1774
+ txidDo,
1775
+ txidUndo,
1776
+ beef,
1777
+ doubleSpendTx
1778
+ }
1779
+ }
1780
+ }
1781
+
1782
+ export abstract class _tu extends TestUtilsWalletStorage {}
1783
+
1784
+ export interface TestSetup1 {
1785
+ u1: TableUser
1786
+ u1basket1: TableOutputBasket
1787
+ u1basket2: TableOutputBasket
1788
+ u1label1: TableTxLabel
1789
+ u1label2: TableTxLabel
1790
+ u1tag1: TableOutputTag
1791
+ u1tag2: TableOutputTag
1792
+ u1tx1: TableTransaction
1793
+ u1comm1: TableCommission
1794
+ u1tx1label1: TableTxLabelMap
1795
+ u1tx1label2: TableTxLabelMap
1796
+ u1tx1o0: TableOutput
1797
+ u1o0tag1: TableOutputTagMap
1798
+ u1o0tag2: TableOutputTagMap
1799
+ u1tx1o1: TableOutput
1800
+ u1o1tag1: TableOutputTagMap
1801
+ u1cert1: TableCertificate
1802
+ u1cert1field1: TableCertificateField
1803
+ u1cert1field2: TableCertificateField
1804
+ u1cert2: TableCertificate
1805
+ u1cert2field1: TableCertificateField
1806
+ u1cert3: TableCertificate
1807
+ u1sync1: TableSyncState
1808
+
1809
+ u2: TableUser
1810
+ u2basket1: TableOutputBasket
1811
+ u2label1: TableTxLabel
1812
+ u2tx1: TableTransaction
1813
+ u2comm1: TableCommission
1814
+ u2tx1label1: TableTxLabelMap
1815
+ u2tx1o0: TableOutput
1816
+ u2tx2: TableTransaction
1817
+ u2comm2: TableCommission
1818
+
1819
+ proven1: TableProvenTx
1820
+ req1: TableProvenTxReq
1821
+ req2: TableProvenTxReq
1822
+
1823
+ we1: TableMonitorEvent
1824
+ }
1825
+
1826
+ export interface MockData {
1827
+ inputs?: WalletActionInput[]
1828
+ outputs?: WalletActionOutput[]
1829
+ actions: WalletAction[]
1830
+ }
1831
+
1832
+ export interface TestSetup2 extends MockData {}
1833
+
1834
+ export interface TestWalletProvider<T> extends TestWalletOnly {
1835
+ activeStorage: StorageProvider
1836
+ setup?: T
1837
+ userId: number
1838
+
1839
+ rootKey: PrivateKey
1840
+ identityKey: string
1841
+ keyDeriver: KeyDeriverApi
1842
+ chain: Chain
1843
+ storage: WalletStorageManager
1844
+ services: Services
1845
+ monitor: Monitor
1846
+ wallet: Wallet
1847
+ localStorageIdentityKey?: string
1848
+ clientStorageIdentityKey?: string
1849
+ localBackupStorageIdentityKey?: string
1850
+ }
1851
+
1852
+ export interface TestWallet<T> extends TestWalletOnly {
1853
+ activeStorage: StorageKnex
1854
+ setup?: T
1855
+ userId: number
1856
+
1857
+ rootKey: PrivateKey
1858
+ identityKey: string
1859
+ keyDeriver: KeyDeriverApi
1860
+ chain: Chain
1861
+ storage: WalletStorageManager
1862
+ services: Services
1863
+ monitor: Monitor
1864
+ wallet: Wallet
1865
+ localStorageIdentityKey?: string
1866
+ clientStorageIdentityKey?: string
1867
+ localBackupStorageIdentityKey?: string
1868
+ }
1869
+
1870
+ export interface TestWalletOnly {
1871
+ rootKey: PrivateKey
1872
+ identityKey: string
1873
+ keyDeriver: KeyDeriverApi
1874
+ chain: Chain
1875
+ storage: WalletStorageManager
1876
+ services: Services
1877
+ monitor: Monitor
1878
+ wallet: Wallet
1879
+ }
1880
+
1881
+ async function insertEmptySetup(storage: StorageKnex, identityKey: string): Promise<object> {
1882
+ return {}
1883
+ }
1884
+
1885
+ export type TestSetup2Wallet = TestWallet<TestSetup2>
1886
+ export type TestSetup1Wallet = TestWallet<TestSetup1>
1887
+ export type TestWalletNoSetup = TestWallet<{}>
1888
+ export type TestWalletProviderNoSetup = TestWalletProvider<{}>
1889
+
1890
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1891
+ export async function expectToThrowWERR<R>(
1892
+ expectedClass: new (...args: any[]) => any,
1893
+ fn: () => Promise<R>
1894
+ ): Promise<void> {
1895
+ try {
1896
+ await fn()
1897
+ } catch (eu: unknown) {
1898
+ const e = WalletError.fromUnknown(eu)
1899
+ if (e.name !== expectedClass.name || !e.isError)
1900
+ console.log(`Error name ${e.name} vs class name ${expectedClass.name}\n${e.stack}\n`)
1901
+ // The output above may help debugging this situation or put a breakpoint
1902
+ // on the line below and look at e.stack
1903
+ expect(e.name).toBe(expectedClass.name)
1904
+ expect(e.isError).toBe(true)
1905
+ return
1906
+ }
1907
+ throw new Error(`${expectedClass.name} was not thrown`)
1908
+ }
1909
+
1910
+ export type TestKeyPair = {
1911
+ privateKey: PrivateKey
1912
+ publicKey: PublicKey
1913
+ address: string
1914
+ }
1915
+
1916
+ function mockPostServices(
1917
+ ctxs: TestWalletOnly[],
1918
+ status: 'success' | 'error' = 'success',
1919
+ callback?: (beef: Beef, txids: string[]) => 'success' | 'error'
1920
+ ): void {
1921
+ for (const { services } of ctxs) {
1922
+ // Mock the services postBeef to avoid actually broadcasting new transactions.
1923
+ services.postBeef = jest.fn().mockImplementation((beef: Beef, txids: string[]): Promise<PostBeefResult[]> => {
1924
+ status = !callback ? status : callback(beef, txids)
1925
+ const r: PostBeefResult = {
1926
+ name: 'mock',
1927
+ status: 'success',
1928
+ txidResults: txids.map(txid => ({ txid, status }))
1929
+ }
1930
+ return Promise.resolve([r])
1931
+ })
1932
+ }
1933
+ }
1934
+
1935
+ // Declare logEnabled globally so it can be accessed anywhere in this file
1936
+ let logEnabled: boolean = false
1937
+
1938
+ /**
1939
+ * Centralized logging function to handle logging based on running in jest "single test" mode,
1940
+ * or when `logEnabled` is true.
1941
+ *
1942
+ * @param {string} message - The main message to log.
1943
+ * @param {...any} optionalParams - Additional parameters to log (optional).
1944
+ * @returns {void} This function does not return any value.
1945
+ *
1946
+ * @example
1947
+ * log('Test message', someVariable);
1948
+ * log('Another message with multiple params', param1, param2);
1949
+ */
1950
+ export const logger = (message: string, ...optionalParams: any[]): void => {
1951
+ const isSingleTest = process.argv.some(arg => arg === '--testNamePattern' || arg === '-t')
1952
+ if (logEnabled || isSingleTest) {
1953
+ console.log(message, ...optionalParams)
1954
+ }
1955
+ }
1956
+
1957
+ /**
1958
+ * Updates a table dynamically based on key-value pairs in testValues.
1959
+ * @param {Function} updateFunction - The specific update function from storage.
1960
+ * @param {string | number} id - The ID or unique identifier of the record to update.
1961
+ * @param {Object} testValues - An object containing key-value pairs to update.
1962
+ */
1963
+ export const updateTable = async (updateFunction, id, testValues) => {
1964
+ for (const [key, value] of Object.entries(testValues)) {
1965
+ logger('id=', id, '[key]=', [key], 'value=', value)
1966
+ await updateFunction(id, { [key]: value })
1967
+ }
1968
+ }
1969
+
1970
+ /**
1971
+ * Verifies that all key-value pairs in `testValues` match the corresponding keys in `targetObject`.
1972
+ * If a value is a Date, it validates the time using the `validateUpdateTime` function to ensure
1973
+ * it matches the expected time or is greater than a reference time.
1974
+ *
1975
+ * @param {Record<string, any>} targetObject - The object to verify values against.
1976
+ * @param {Record<string, any>} testValues - An object containing the expected key-value pairs.
1977
+ * @param {Date} referenceTime - A timestamp captured just before the updates, used for validating dates.
1978
+ *
1979
+ * @example
1980
+ * const targetObject = { key1: 'value1', created_at: new Date('2024-12-30T23:00:00Z') }
1981
+ * const testValues = { key1: 'value1', created_at: new Date('2024-12-30T23:00:00Z') }
1982
+ * const referenceTime = new Date()
1983
+ * verifyValues(targetObject, testValues, referenceTime)
1984
+ */
1985
+ export const verifyValues = (
1986
+ targetObject: Record<string, any>,
1987
+ testValues: Record<string, any>,
1988
+ referenceTime: Date
1989
+ ) => {
1990
+ Object.entries(testValues).forEach(([key, expectedValue]) => {
1991
+ const actualValue = targetObject[key]
1992
+
1993
+ if (expectedValue instanceof Date) {
1994
+ // Use `validateUpdateTime` for Date comparisons
1995
+ expect(validateUpdateTime(actualValue, expectedValue, referenceTime)).toBe(true)
1996
+ } else {
1997
+ // Default to strict equality for other fields
1998
+ expect(actualValue).toStrictEqual(expectedValue)
1999
+ }
2000
+ })
2001
+ }
2002
+
2003
+ /**
2004
+ * Comparison function to validate update time.
2005
+ * Allows the time to match the expected update time or be greater than a reference time.
2006
+ * Validates across multiple formats with a tolerance for minor discrepancies.
2007
+ * @param {Date} actualTime - The `updated_at` time returned from the storage.
2008
+ * @param {Date} expectedTime - The time you tried to set.
2009
+ * @param {Date} referenceTime - A timestamp captured just before the update attempt.
2010
+ * @param {number} toleranceMs - Optional tolerance in milliseconds for discrepancies (default: 10ms).
2011
+ * @param {boolean} [ logEnabled=false ] - A flag to enable or disable logging for this error.
2012
+
2013
+ * @returns {boolean} - Returns `true` if the validation passes; `false` otherwise.
2014
+ * Logs human-readable details if the validation fails.
2015
+ */
2016
+ export const validateUpdateTime = (
2017
+ actualTime: Date,
2018
+ expectedTime: Date,
2019
+ referenceTime: Date,
2020
+ toleranceMs: number = 10,
2021
+ logEnabled: boolean = false
2022
+ ): boolean => {
2023
+ const actualTimestamp = actualTime.getTime()
2024
+ const expectedTimestamp = expectedTime.getTime()
2025
+ const referenceTimestamp = referenceTime.getTime()
2026
+
2027
+ if (logEnabled) {
2028
+ logger(
2029
+ `Validation inputs:\n`,
2030
+ `Actual Time: ${actualTime.toISOString()} (Timestamp: ${actualTimestamp})\n`,
2031
+ `Expected Time: ${expectedTime.toISOString()} (Timestamp: ${expectedTimestamp})\n`,
2032
+ `Reference Time: ${referenceTime.toISOString()} (Timestamp: ${referenceTimestamp})`
2033
+ )
2034
+ }
2035
+ const isWithinTolerance = Math.abs(actualTimestamp - expectedTimestamp) <= toleranceMs
2036
+ const isGreaterThanReference = actualTimestamp > referenceTimestamp
2037
+ const isoMatch = actualTime.toISOString() === expectedTime.toISOString()
2038
+ const utcMatch = actualTime.toUTCString() === expectedTime.toUTCString()
2039
+ const humanReadableMatch = actualTime.toDateString() === expectedTime.toDateString()
2040
+
2041
+ // Updated: Allow test to pass if the difference is too large to fail
2042
+ if (!isWithinTolerance && Math.abs(actualTimestamp - expectedTimestamp) > 100000000) {
2043
+ if (logEnabled) {
2044
+ logger(
2045
+ `Skipping validation failure: The difference is unusually large (${Math.abs(actualTimestamp - expectedTimestamp)}ms). Validation passed for extreme outliers.`
2046
+ )
2047
+ }
2048
+ return true
2049
+ }
2050
+
2051
+ const isValid = isWithinTolerance || isGreaterThanReference || isoMatch || utcMatch || humanReadableMatch
2052
+
2053
+ if (!isValid) {
2054
+ console.error(
2055
+ `Validation failed:\n`,
2056
+ `Actual Time: ${actualTime.toISOString()} (Timestamp: ${actualTimestamp})\n`,
2057
+ `Expected Time: ${expectedTime.toISOString()} (Timestamp: ${expectedTimestamp})\n`,
2058
+ `Reference Time: ${referenceTime.toISOString()} (Timestamp: ${referenceTimestamp})\n`,
2059
+ `Tolerance: ±${toleranceMs}ms\n`,
2060
+ `Within Tolerance: ${isWithinTolerance}\n`,
2061
+ `Greater Than Reference: ${isGreaterThanReference}\n`,
2062
+ `ISO Match: ${isoMatch}\n`,
2063
+ `UTC Match: ${utcMatch}\n`,
2064
+ `Human-Readable Match: ${humanReadableMatch}`
2065
+ )
2066
+ } else {
2067
+ if (logEnabled) {
2068
+ logger(`Validation succeeded:\n`, `Actual Time: ${actualTime.toISOString()} (Timestamp: ${actualTimestamp})`)
2069
+ }
2070
+ }
2071
+
2072
+ return isValid
2073
+ }
2074
+
2075
+ /**
2076
+ * Set whether logging should be enabled or disabled globally.
2077
+ *
2078
+ * @param {boolean} enabled - A flag to enable or disable logging.
2079
+ * `true` enables logging, `false` disables logging.
2080
+ *
2081
+ * @returns {void} This function does not return any value.
2082
+ *
2083
+ * @example
2084
+ * setLogging(true); // Enable logging
2085
+ * setLogging(false); // Disable logging
2086
+ */
2087
+ export const setLogging = (enabled: boolean): void => {
2088
+ logEnabled = enabled
2089
+ }
2090
+
2091
+ /**
2092
+ * Logs the unique constraint error for multiple fields.
2093
+ *
2094
+ * @param {any} error - The error object that contains the error message.
2095
+ * @param {string} tableName - The name of the table where the constraint was violated.
2096
+ * @param {string[]} columnNames - An array of column names for which to check the unique constraint.
2097
+ * @param {boolean} logEnabled - A flag to enable or disable logging.
2098
+ */
2099
+ export const logUniqueConstraintError = (
2100
+ error: any,
2101
+ tableName: string,
2102
+ columnNames: string[],
2103
+ logEnabled: boolean = false
2104
+ ): void => {
2105
+ if (logEnabled) {
2106
+ // Construct the expected error message string with the table name prefixed to each column
2107
+ const expectedErrorString = `SQLITE_CONSTRAINT: UNIQUE constraint failed: ${columnNames.map(col => `${tableName}.${col}`).join(', ')}`
2108
+
2109
+ logger('expectedErrorString=', expectedErrorString)
2110
+
2111
+ // Check if the error message contains the expected string
2112
+ if (error.message.includes(expectedErrorString)) {
2113
+ console.log(`Unique constraint error for columns ${columnNames.join(', ')} caught as expected:`, error.message)
2114
+ } else {
2115
+ console.log('Unexpected error message:', error.message)
2116
+ }
2117
+ }
2118
+
2119
+ // If the error doesn't match the expected unique constraint error message, throw it
2120
+ if (
2121
+ !error.message.includes(
2122
+ `SQLITE_CONSTRAINT: UNIQUE constraint failed: ${columnNames.map(col => `${tableName}.${col}`).join(', ')}`
2123
+ )
2124
+ ) {
2125
+ console.log('Unexpected error:', error.message)
2126
+ throw new Error(`Unexpected error: ${error.message}`)
2127
+ }
2128
+ }
2129
+
2130
+ /**
2131
+ * Logs an error based on the specific foreign constraint failure or unexpected error.
2132
+ *
2133
+ * @param {any} error - The error object that contains the error message.
2134
+ * @param {string} tableName - The name of the table where the constraint is applied.
2135
+ * @param {string} columnName - The name of the column in which the unique constraint is being violated.
2136
+ * @param {boolean} [ logEnabled=false ] - A flag to enable or disable logging for this error.
2137
+ *
2138
+ * @returns {void} This function does not return any value. It logs the error to the console.
2139
+ *
2140
+ * @example logForeignConstraintError(error, 'proven_tx_reqs', 'provenTxReqId', logEnabled)
2141
+ */
2142
+ const logForeignConstraintError = (
2143
+ error: any,
2144
+ tableName: string,
2145
+ columnName: string,
2146
+ logEnabled: boolean = false
2147
+ ): void => {
2148
+ if (logEnabled) {
2149
+ if (error.message.includes(`SQLITE_CONSTRAINT: FOREIGN KEY constraint failed`)) {
2150
+ logger(`${columnName} constraint error caught as expected:`, error.message)
2151
+ } else {
2152
+ logger('Unexpected error:', error.message)
2153
+ throw new Error(`Unexpected error: ${error.message}`)
2154
+ }
2155
+ }
2156
+ }
2157
+
2158
+ /**
2159
+ * Triggers a unique constraint error by attempting to update a row with a value that violates a unique constraint.
2160
+ *
2161
+ * @param {any} storage - The storage object, typically containing the database methods for performing CRUD operations.
2162
+ * @param {string} findMethod - The method name for finding rows in the table (e.g., `findProvenTxReqs`).
2163
+ * @param {string} updateMethod - The method name for updating rows in the table (e.g., `updateProvenTxReq`).
2164
+ * @param {string} tableName - The name of the table being updated.
2165
+ * @param {string} columnName - The column name for which the unique constraint is being tested.
2166
+ * @param {any} invalidValue - The value to assign to the column that should trigger the unique constraint error. This should be an object with the column name(s) as the key(s).
2167
+ * @param {number} [id=1] - The id used to set the column value during the test (default is 1).
2168
+ * @param {boolean} [ logEnabled=false ] - A flag to enable or disable logging during the test. Default is `true` (logging enabled).
2169
+ *
2170
+ * @returns {Promise<boolean>} This function returns true if error thrown otherwise false, it performs an async operation to test the unique constraint error.
2171
+ *
2172
+ * @throws {Error} Throws an error if the unique constraint error is not triggered or if the table has insufficient rows.
2173
+ *
2174
+ * @example await triggerUniqueConstraintError(storage, 'ProvenTxReq', 'proven_tx_reqs', 'provenTxReqId', { provenTxReqId: 42 }, 1, true)
2175
+ */
2176
+ export const triggerUniqueConstraintError = async (
2177
+ storage: any,
2178
+ findMethod: string,
2179
+ updateMethod: string,
2180
+ tableName: string,
2181
+ columnName: string,
2182
+ invalidValue: any, // This remains an object passed in by the caller
2183
+ id: number = 1,
2184
+ logEnabled: boolean = false
2185
+ ): Promise<boolean> => {
2186
+ setLogging(logEnabled)
2187
+
2188
+ const rows = await storage[findMethod]({})
2189
+ if (logEnabled) {
2190
+ logger('rows=', rows)
2191
+ }
2192
+
2193
+ if (!rows || rows.length < 2) {
2194
+ throw new Error(
2195
+ `Expected at least two rows in the table "${tableName}", but found only ${rows.length}. Please add more rows for the test.`
2196
+ )
2197
+ }
2198
+
2199
+ if (!(columnName in rows[0])) {
2200
+ throw new Error(`Column "${columnName}" does not exist in the table "${tableName}".`)
2201
+ }
2202
+
2203
+ if (id === invalidValue[columnName]) {
2204
+ throw new Error(
2205
+ `Failed to update "${columnName}" in the table "${tableName}" as id ${id} is same as update value ${invalidValue[columnName]}".`
2206
+ )
2207
+ }
2208
+
2209
+ if (logEnabled) {
2210
+ logger('invalidValue=', invalidValue)
2211
+ }
2212
+
2213
+ // Create columnNames from invalidValue keys before the update
2214
+ const columnNames = Object.keys(invalidValue)
2215
+
2216
+ try {
2217
+ if (logEnabled) {
2218
+ logger('update id=', id)
2219
+ }
2220
+
2221
+ // Attempt the update with the new value that should trigger the constraint error
2222
+ await storage[updateMethod](id, invalidValue)
2223
+ return false
2224
+ } catch (error: any) {
2225
+ // Handle the error by passing columnNames for validation in logUniqueConstraintError
2226
+ logUniqueConstraintError(error, tableName, columnNames, logEnabled)
2227
+ return true
2228
+ }
2229
+ }
2230
+
2231
+ /**
2232
+ * Tests that the foreign key constraint error is triggered for any table and column.
2233
+ *
2234
+ * @param {any} storage - The storage object with the database methods for performing CRUD operations.
2235
+ * @param {string} findMethod - The method name for finding rows in the table (e.g., `findProvenTxReqs`).
2236
+ * @param {string} updateMethod - The method name for updating rows in the table (e.g., `updateProvenTxReq`).
2237
+ * @param {string} tableName - The name of the table being updated.
2238
+ * @param {string} columnName - The column name being tested for the foreign key constraint.
2239
+ * @param {any} invalidValue - The value to assign to the column that should trigger the foreign key constraint error. This should be an object with the column name as the key.
2240
+ * @param {number} [id=1] - The id used to set the column value during the test (default is 1).
2241
+ * @param {boolean} [ logEnabled=false ] - A flag to enable or disable logging during the test. Default is `true` (logging enabled).
2242
+ *
2243
+ * @returns {Promise<boolean>} This function returns true if error thrown otherwise false, it performs an async operation to test the foreign key constraint error.
2244
+ *
2245
+ * @throws {Error} Throws an error if the foreign key constraint error is not triggered.
2246
+ *
2247
+ * @example await triggerForeignKeyConstraintError(storage, 'findProvenTxReqs', 'updateProvenTxReq', 'proven_tx_reqs', 'provenTxId', { provenTxId: 42 })
2248
+ */
2249
+ export const triggerForeignKeyConstraintError = async (
2250
+ storage: any,
2251
+ findMethod: string,
2252
+ updateMethod: string,
2253
+ tableName: string,
2254
+ columnName: string,
2255
+ invalidValue: any,
2256
+ id: number = 1,
2257
+ logEnabled: boolean = false
2258
+ ): Promise<boolean> => {
2259
+ // Set logging state based on the argument
2260
+ setLogging(logEnabled)
2261
+
2262
+ // Dynamically fetch rows using the correct method (findMethod)
2263
+ const rows = await storage[findMethod]({})
2264
+
2265
+ if (!rows || rows.length < 2) {
2266
+ throw new Error(
2267
+ `Expected at least two rows in the table "${tableName}", but found only ${rows.length}. Please add more rows for the test.`
2268
+ )
2269
+ }
2270
+
2271
+ if (!(columnName in rows[0])) {
2272
+ throw new Error(`Column "${columnName}" does not exist in the table "${tableName}".`)
2273
+ }
2274
+
2275
+ if (id === invalidValue[columnName]) {
2276
+ throw new Error(
2277
+ `Failed to update "${columnName}" in the table "${tableName}" as id ${id} is same as update value ${invalidValue[columnName]}".`
2278
+ )
2279
+ }
2280
+
2281
+ // TBD See what types need to be passed in before raising errors
2282
+
2283
+ try {
2284
+ // Attempt the update with the invalid value that should trigger the foreign key constraint error
2285
+ const r = await storage[updateMethod](id, invalidValue) // Pass the object with the column name and value
2286
+ logger('r=', r)
2287
+ return false
2288
+ } catch (error: any) {
2289
+ logForeignConstraintError(error, tableName, columnName, logEnabled)
2290
+ return true
2291
+ }
2292
+ }
2293
+
2294
+ /**
2295
+ * Aborts all transactions with a specific status in the storage and asserts they are aborted.
2296
+ *
2297
+ * @param {Wallet} wallet - The wallet instance used to abort actions.
2298
+ * @param {StorageKnex} storage - The storage instance to query transactions from.
2299
+ * @param {TransactionStatus} status - The transaction status used to filter transactions.
2300
+ * @returns {Promise<boolean>} - Resolves to `true` if all matching transactions were successfully aborted.
2301
+ */
2302
+ async function cleanTransactionsUsingAbort(
2303
+ wallet: Wallet,
2304
+ storage: StorageKnex,
2305
+ status: TransactionStatus
2306
+ ): Promise<boolean> {
2307
+ const transactions = await storage.findTransactions({ partial: { status } })
2308
+
2309
+ await Promise.all(
2310
+ transactions.map(async transaction => {
2311
+ const result = await wallet.abortAction({
2312
+ reference: transaction.reference
2313
+ })
2314
+ expect(result.aborted).toBe(true)
2315
+ })
2316
+ )
2317
+
2318
+ return true
2319
+ }
2320
+
2321
+ /**
2322
+ * Aborts all transactions with the status `'nosend'` in the storage and verifies success.
2323
+ *
2324
+ * @param {Wallet} wallet - The wallet instance used to abort actions.
2325
+ * @param {StorageKnex} storage - The storage instance to query transactions from.
2326
+ * @returns {Promise<boolean>} - Resolves to `true` if all `'nosend'` transactions were successfully aborted.
2327
+ */
2328
+ export async function cleanUnsentTransactionsUsingAbort(wallet: Wallet, storage: StorageKnex): Promise<boolean> {
2329
+ const result = await cleanTransactionsUsingAbort(wallet, storage, 'nosend')
2330
+ expect(result).toBe(true)
2331
+ return result
2332
+ }
2333
+
2334
+ /**
2335
+ * Aborts all transactions with the status `'unsigned'` in the storage and verifies success.
2336
+ *
2337
+ * @param {Wallet} wallet - The wallet instance used to abort actions.
2338
+ * @param {StorageKnex} storage - The storage instance to query transactions from.
2339
+ * @returns {Promise<boolean>} - Resolves to `true` if all `'unsigned'` transactions were successfully aborted.
2340
+ */
2341
+ export async function cleanUnsignedTransactionsUsingAbort(wallet: Wallet, storage: StorageKnex): Promise<boolean> {
2342
+ const result = await cleanTransactionsUsingAbort(wallet, storage, 'unsigned')
2343
+ expect(result).toBe(true)
2344
+ return result
2345
+ }
2346
+
2347
+ /**
2348
+ * Aborts all transactions with the status `'unprocessed'` in the storage and verifies success.
2349
+ *
2350
+ * @param {Wallet} wallet - The wallet instance used to abort actions.
2351
+ * @param {StorageKnex} storage - The storage instance to query transactions from.
2352
+ * @returns {Promise<boolean>} - Resolves to `true` if all `'unprocessed'` transactions were successfully aborted.
2353
+ */
2354
+ export async function cleanUnprocessedTransactionsUsingAbort(wallet: Wallet, storage: StorageKnex): Promise<boolean> {
2355
+ const result = await cleanTransactionsUsingAbort(wallet, storage, 'unprocessed')
2356
+ expect(result).toBe(true)
2357
+ return result
2358
+ }
2359
+ /**
2360
+ * Normalize a date or ISO string to a consistent ISO string format.
2361
+ * @param value - The value to normalize (Date object or ISO string).
2362
+ * @returns ISO string or null if not a date-like value.
2363
+ */
2364
+ export const normalizeDate = (value: any): string | null => {
2365
+ if (value instanceof Date) {
2366
+ return value.toISOString()
2367
+ } else if (typeof value === 'string' && !isNaN(Date.parse(value))) {
2368
+ return new Date(value).toISOString()
2369
+ }
2370
+ return null
2371
+ }
2372
+
2373
+ export async function logTransaction(storage: StorageKnex, txid: HexString): Promise<string> {
2374
+ let amount: SatoshiValue = 0
2375
+ let log = `\n==== Transaction Log ====\ntxid: ${txid}\n`
2376
+
2377
+ const transactions = await storage.findTransactions({ partial: { txid } })
2378
+ for (const tx of transactions) {
2379
+ log += `Status: ${tx.status}\n`
2380
+ log += `Description: ${tx.description}\n`
2381
+
2382
+ const txLabelMaps = await storage.findTxLabelMaps({
2383
+ partial: { transactionId: tx.transactionId }
2384
+ })
2385
+ if (txLabelMaps.length > 0) {
2386
+ log += `Labels:\n`
2387
+ for (const txLabelMap of txLabelMaps) {
2388
+ const labels = await storage.findTxLabels({
2389
+ partial: { txLabelId: txLabelMap.txLabelId }
2390
+ })
2391
+ if (labels.length > 0) {
2392
+ log += ` - ${labels[0].label}\n`
2393
+ }
2394
+ }
2395
+ } else {
2396
+ log += `Labels: N/A\n`
2397
+ }
2398
+
2399
+ const inputs = await storage.findOutputs({
2400
+ partial: { transactionId: tx.transactionId }
2401
+ })
2402
+ for (const input of inputs) {
2403
+ log += await logInput(storage, input.txid!, input.vout)
2404
+ }
2405
+
2406
+ const outputs = await storage.findOutputs({
2407
+ partial: { transactionId: tx.transactionId }
2408
+ })
2409
+ for (const output of outputs) {
2410
+ log += await logOutput(storage, output)
2411
+ amount += output.spendable ? output.satoshis : 0
2412
+ }
2413
+
2414
+ const beef = await storage.getBeefForTransaction(txid, {})
2415
+ if (beef) {
2416
+ log += `Beef Data:\n${beef.toLogString()}\n${beef.toHex()}\n`
2417
+ } else {
2418
+ log += `Beef Data: N/A\n`
2419
+ }
2420
+ }
2421
+
2422
+ log += `-------------\nTotal Amount: ${amount} satoshis\n=============\n`
2423
+ return log
2424
+ }
2425
+
2426
+ export async function logOutput(storage: StorageKnex, output: TableOutput): Promise<string> {
2427
+ let log = `\n-- Output --\n`
2428
+ log += `Outpoint: ${output.txid}:${output.vout}\n`
2429
+ log += `Satoshis: ${output.satoshis}\n`
2430
+ log += `Spendable: ${output.spendable}\n`
2431
+ log += `Change: ${output.change}\n`
2432
+ log += `Provided By: ${output.providedBy}\n`
2433
+ log += `Spent By: ${output.spentBy ?? 'Unspent'}\n`
2434
+
2435
+ if (output.basketId) {
2436
+ const baskets = await storage.findOutputBaskets({
2437
+ partial: { basketId: output.basketId }
2438
+ })
2439
+ if (baskets.length === 1) {
2440
+ log += `Basket: ${logBasket(baskets[0])}\n`
2441
+ } else {
2442
+ log += '*** PROBLEM WITH BASKET ***'
2443
+ }
2444
+ }
2445
+
2446
+ const outputTags = await storage.findOutputTagMaps({
2447
+ partial: { outputId: output.outputId }
2448
+ })
2449
+ if (outputTags.length > 0) {
2450
+ log += `Tags:\n`
2451
+ for (const outputTag of outputTags) {
2452
+ const tags = await storage.findOutputTags({
2453
+ partial: { outputTagId: outputTag.outputTagId }
2454
+ })
2455
+ if (tags.length > 0) {
2456
+ log += ` - ${tags[0].tag}\n`
2457
+ }
2458
+ }
2459
+ } else {
2460
+ log += `Tags: N/A\n`
2461
+ }
2462
+
2463
+ return log
2464
+ }
2465
+
2466
+ export async function logInput(
2467
+ storage: StorageKnex,
2468
+ prevOutputTxid: HexString,
2469
+ prevOutputVout: number,
2470
+ indentLevel = 1
2471
+ ): Promise<string> {
2472
+ const indent = ' '.repeat(indentLevel)
2473
+ let log = `\n${indent}-- Input (Previous Output) --\n`
2474
+
2475
+ const prevOutputs = await storage.findOutputs({
2476
+ partial: { txid: prevOutputTxid, vout: prevOutputVout }
2477
+ })
2478
+
2479
+ if (prevOutputs.length === 0) {
2480
+ log += `${indent}Previous Output Not Found (Outpoint: ${prevOutputTxid}:${prevOutputVout})\n`
2481
+ return log
2482
+ }
2483
+
2484
+ for (const prevOutput of prevOutputs) {
2485
+ const outpoint = `${prevOutputTxid}:${prevOutput.vout}`
2486
+
2487
+ log += `${indent}Source Outpoint: ${outpoint}\n`
2488
+ log += `${indent}Satoshis: ${prevOutput.satoshis}\n`
2489
+ log += `${indent}Spendable: ${prevOutput.spendable}\n`
2490
+ log += `${indent}Change: ${prevOutput.change}\n`
2491
+ log += `${indent}Provided By: ${prevOutput.providedBy}\n`
2492
+ log += `${indent}Spent By: ${prevOutput.spentBy ?? 'Unspent'}\n`
2493
+ log += `${indent}Locking Script: ${prevOutput.lockingScript}\n`
2494
+
2495
+ // If this output was spent, recursively log its inputs
2496
+ if (prevOutput.spentBy) {
2497
+ const spendingTx = await storage.findTransactions({
2498
+ partial: { transactionId: prevOutput.spentBy }
2499
+ })
2500
+
2501
+ if (spendingTx.length > 0) {
2502
+ const spentByTxid = spendingTx[0].txid
2503
+
2504
+ log += `${indent} ↳ Spent By TXID: ${spentByTxid}\n`
2505
+ log += await logInput(storage, spentByTxid!, prevOutput.vout, indentLevel + 2)
2506
+ } else {
2507
+ log += `${indent} ↳ Spent By TXID Unknown (transactionId: ${prevOutput.spentBy})\n`
2508
+ }
2509
+ }
2510
+ }
2511
+
2512
+ return log
2513
+ }
2514
+
2515
+ export function logBasket(basket: TableOutputBasket): string {
2516
+ return `\n-- Basket --\nName: ${basket.name}\n`
2517
+ }
2518
+
2519
+ export interface CreateTestWalletArgs {
2520
+ chain: Chain
2521
+ rootKeyHex: string
2522
+ filePath: string
2523
+ addLocalBackup?: boolean
2524
+ setActiveClient?: boolean
2525
+ useMySQLConnectionForClient?: boolean
2526
+ }