beignet 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc +40 -0
  3. package/.prettierrc +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +166 -0
  6. package/dist/electrum/constants.js +31 -0
  7. package/dist/electrum/constants.js.map +1 -0
  8. package/dist/electrum/index.js +552 -0
  9. package/dist/electrum/index.js.map +1 -0
  10. package/dist/index.js +23 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/shapes/electrum.js +14 -0
  13. package/dist/shapes/electrum.js.map +1 -0
  14. package/dist/shapes/index.js +19 -0
  15. package/dist/shapes/index.js.map +1 -0
  16. package/dist/shapes/wallet.js +115 -0
  17. package/dist/shapes/wallet.js.map +1 -0
  18. package/dist/transaction/index.js +692 -0
  19. package/dist/transaction/index.js.map +1 -0
  20. package/dist/types/electrum/constants.d.ts +7 -0
  21. package/dist/types/electrum/index.d.ts +85 -0
  22. package/dist/types/electrum.js +9 -0
  23. package/dist/types/electrum.js.map +1 -0
  24. package/dist/types/index.d.ts +6 -0
  25. package/dist/types/index.js +20 -0
  26. package/dist/types/index.js.map +1 -0
  27. package/dist/types/shapes/electrum.d.ts +5 -0
  28. package/dist/types/shapes/index.d.ts +2 -0
  29. package/dist/types/shapes/wallet.d.ts +10 -0
  30. package/dist/types/transaction/index.d.ts +92 -0
  31. package/dist/types/transaction.js +12 -0
  32. package/dist/types/transaction.js.map +1 -0
  33. package/dist/types/types/electrum.d.ts +113 -0
  34. package/dist/types/types/index.d.ts +3 -0
  35. package/dist/types/types/transaction.d.ts +51 -0
  36. package/dist/types/types/wallet.d.ts +400 -0
  37. package/dist/types/utils/conversion.d.ts +10 -0
  38. package/dist/types/utils/derivation-path.d.ts +19 -0
  39. package/dist/types/utils/electrum.d.ts +10 -0
  40. package/dist/types/utils/exchange-rate.d.ts +30 -0
  41. package/dist/types/utils/helpers.d.ts +23 -0
  42. package/dist/types/utils/index.d.ts +7 -0
  43. package/dist/types/utils/result.d.ts +16 -0
  44. package/dist/types/utils/transaction.d.ts +21 -0
  45. package/dist/types/utils/wallet.d.ts +47 -0
  46. package/dist/types/wallet/constants.d.ts +12 -0
  47. package/dist/types/wallet/index.d.ts +214 -0
  48. package/dist/types/wallet.js +36 -0
  49. package/dist/types/wallet.js.map +1 -0
  50. package/dist/utils/conversion.js +52 -0
  51. package/dist/utils/conversion.js.map +1 -0
  52. package/dist/utils/derivation-path.js +104 -0
  53. package/dist/utils/derivation-path.js.map +1 -0
  54. package/dist/utils/electrum.js +149 -0
  55. package/dist/utils/electrum.js.map +1 -0
  56. package/dist/utils/exchange-rate.js +49 -0
  57. package/dist/utils/exchange-rate.js.map +1 -0
  58. package/dist/utils/helpers.js +137 -0
  59. package/dist/utils/helpers.js.map +1 -0
  60. package/dist/utils/index.js +24 -0
  61. package/dist/utils/index.js.map +1 -0
  62. package/dist/utils/result.js +36 -0
  63. package/dist/utils/result.js.map +1 -0
  64. package/dist/utils/transaction.js +327 -0
  65. package/dist/utils/transaction.js.map +1 -0
  66. package/dist/utils/wallet.js +227 -0
  67. package/dist/utils/wallet.js.map +1 -0
  68. package/dist/wallet/constants.js +15 -0
  69. package/dist/wallet/constants.js.map +1 -0
  70. package/dist/wallet/index.js +2113 -0
  71. package/dist/wallet/index.js.map +1 -0
  72. package/docs/html/.nojekyll +1 -0
  73. package/docs/html/assets/highlight.css +22 -0
  74. package/docs/html/assets/main.js +58 -0
  75. package/docs/html/assets/search.js +1 -0
  76. package/docs/html/assets/style.css +1367 -0
  77. package/docs/html/classes/Electrum.html +812 -0
  78. package/docs/html/classes/Transaction.html +687 -0
  79. package/docs/html/classes/Wallet.html +2030 -0
  80. package/docs/html/enums/EAddressType.html +227 -0
  81. package/docs/html/enums/EAvailableNetworks.html +255 -0
  82. package/docs/html/enums/EBoostType.html +220 -0
  83. package/docs/html/enums/EFeeId.html +241 -0
  84. package/docs/html/enums/EPaymentType.html +220 -0
  85. package/docs/html/enums/EProtocol.html +220 -0
  86. package/docs/html/enums/EUnit.html +227 -0
  87. package/docs/html/functions/availableNetworks.html +200 -0
  88. package/docs/html/functions/constructByteCountParam.html +210 -0
  89. package/docs/html/functions/decodeOpReturnMessage.html +202 -0
  90. package/docs/html/functions/decodeRawTransaction.html +210 -0
  91. package/docs/html/functions/err.html +214 -0
  92. package/docs/html/functions/formatKeyDerivationPath.html +219 -0
  93. package/docs/html/functions/formatPeerData.html +207 -0
  94. package/docs/html/functions/generateMnemonic.html +223 -0
  95. package/docs/html/functions/generateWalletId.html +202 -0
  96. package/docs/html/functions/getAddressFromScriptPubKey.html +209 -0
  97. package/docs/html/functions/getAddressTypeFromPath.html +206 -0
  98. package/docs/html/functions/getByteCount.html +206 -0
  99. package/docs/html/functions/getDataFallback.html +207 -0
  100. package/docs/html/functions/getDefaultPort.html +209 -0
  101. package/docs/html/functions/getDefaultWalletData.html +200 -0
  102. package/docs/html/functions/getDefaultWalletDataKeys.html +200 -0
  103. package/docs/html/functions/getExchangeRates.html +200 -0
  104. package/docs/html/functions/getHighestUsedIndexFromTxHashes.html +216 -0
  105. package/docs/html/functions/getKeyDerivationPath.html +211 -0
  106. package/docs/html/functions/getKeyDerivationPathObject.html +216 -0
  107. package/docs/html/functions/getKeyDerivationPathString.html +220 -0
  108. package/docs/html/functions/getKeyValue.html +206 -0
  109. package/docs/html/functions/getPeers.html +211 -0
  110. package/docs/html/functions/getProtocolForPort.html +209 -0
  111. package/docs/html/functions/getScriptHash.html +210 -0
  112. package/docs/html/functions/getSeed.html +209 -0
  113. package/docs/html/functions/getSeedHash.html +206 -0
  114. package/docs/html/functions/getSha256.html +206 -0
  115. package/docs/html/functions/getStorageKeyValues.html +209 -0
  116. package/docs/html/functions/getTxFee.html +210 -0
  117. package/docs/html/functions/getWalletDataStorageKey.html +206 -0
  118. package/docs/html/functions/isValidBech32mEncodedString.html +211 -0
  119. package/docs/html/functions/objectKeys-1.html +211 -0
  120. package/docs/html/functions/objectsMatch.html +210 -0
  121. package/docs/html/functions/ok.html +214 -0
  122. package/docs/html/functions/parseOnChainPaymentRequest.html +204 -0
  123. package/docs/html/functions/reduceValue.html +215 -0
  124. package/docs/html/functions/removeDustOutputs.html +206 -0
  125. package/docs/html/functions/setReplaceByFee.html +210 -0
  126. package/docs/html/functions/shuffleArray.html +211 -0
  127. package/docs/html/functions/validateAddress.html +216 -0
  128. package/docs/html/functions/validateMnemonic.html +206 -0
  129. package/docs/html/functions/validateTransaction.html +206 -0
  130. package/docs/html/index.html +360 -0
  131. package/docs/html/interfaces/IAddInput.html +231 -0
  132. package/docs/html/interfaces/IAddress.html +247 -0
  133. package/docs/html/interfaces/IAddressData.html +231 -0
  134. package/docs/html/interfaces/IAddressType.html +199 -0
  135. package/docs/html/interfaces/IAddressTypeData.html +252 -0
  136. package/docs/html/interfaces/IAddresses.html +199 -0
  137. package/docs/html/interfaces/IBoostedTransaction.html +238 -0
  138. package/docs/html/interfaces/IBoostedTransactions.html +199 -0
  139. package/docs/html/interfaces/ICreateTransaction.html +224 -0
  140. package/docs/html/interfaces/ICustomGetAddress.html +231 -0
  141. package/docs/html/interfaces/ICustomGetScriptHash.html +224 -0
  142. package/docs/html/interfaces/IElectrumGetAddressBalanceRes.html +235 -0
  143. package/docs/html/interfaces/IExchangeRates.html +210 -0
  144. package/docs/html/interfaces/IFormattedPeerData.html +245 -0
  145. package/docs/html/interfaces/IFormattedTransaction.html +343 -0
  146. package/docs/html/interfaces/IFormattedTransactions.html +199 -0
  147. package/docs/html/interfaces/IGenerateAddresses.html +252 -0
  148. package/docs/html/interfaces/IGenerateAddressesResponse.html +224 -0
  149. package/docs/html/interfaces/IGetAddress.html +231 -0
  150. package/docs/html/interfaces/IGetAddressBalanceRes.html +226 -0
  151. package/docs/html/interfaces/IGetAddressByPath.html +224 -0
  152. package/docs/html/interfaces/IGetAddressHistoryResponse.html +269 -0
  153. package/docs/html/interfaces/IGetAddressResponse.html +231 -0
  154. package/docs/html/interfaces/IGetAddressScriptHashesHistoryResponse.html +245 -0
  155. package/docs/html/interfaces/IGetDerivationPath.html +259 -0
  156. package/docs/html/interfaces/IGetFeeEstimatesResponse.html +238 -0
  157. package/docs/html/interfaces/IGetHeaderResponse.html +245 -0
  158. package/docs/html/interfaces/IGetNextAvailableAddressResponse.html +238 -0
  159. package/docs/html/interfaces/IGetTransactions.html +245 -0
  160. package/docs/html/interfaces/IGetTransactionsFromInputs.html +245 -0
  161. package/docs/html/interfaces/IGetUtxosResponse.html +224 -0
  162. package/docs/html/interfaces/IHeader.html +231 -0
  163. package/docs/html/interfaces/IIndexes.html +238 -0
  164. package/docs/html/interfaces/IKeyDerivationPath.html +247 -0
  165. package/docs/html/interfaces/IKeyDerivationPathData.html +224 -0
  166. package/docs/html/interfaces/INewBlock.html +224 -0
  167. package/docs/html/interfaces/IOnchainFees.html +245 -0
  168. package/docs/html/interfaces/IOutput.html +231 -0
  169. package/docs/html/interfaces/IPeerData.html +231 -0
  170. package/docs/html/interfaces/IRbfData.html +252 -0
  171. package/docs/html/interfaces/ISend.html +224 -0
  172. package/docs/html/interfaces/ISendTransaction.html +322 -0
  173. package/docs/html/interfaces/ISendTx.html +231 -0
  174. package/docs/html/interfaces/ISetupTransaction.html +238 -0
  175. package/docs/html/interfaces/ISubscribeToAddress.html +247 -0
  176. package/docs/html/interfaces/ISubscribeToHeader.html +245 -0
  177. package/docs/html/interfaces/ITargets.html +238 -0
  178. package/docs/html/interfaces/ITransaction.html +281 -0
  179. package/docs/html/interfaces/ITxHash.html +217 -0
  180. package/docs/html/interfaces/ITxHashes.html +235 -0
  181. package/docs/html/interfaces/IUtxo.html +275 -0
  182. package/docs/html/interfaces/IVin.html +252 -0
  183. package/docs/html/interfaces/IVout.html +246 -0
  184. package/docs/html/interfaces/IWallet.html +350 -0
  185. package/docs/html/interfaces/IWalletData.html +343 -0
  186. package/docs/html/types/ElectrumConnectionPubSub.html +233 -0
  187. package/docs/html/types/ElectrumConnectionSubscription.html +204 -0
  188. package/docs/html/types/InputData.html +203 -0
  189. package/docs/html/types/ObjectKeys.html +198 -0
  190. package/docs/html/types/Result.html +201 -0
  191. package/docs/html/types/TAddressIndexInfo.html +204 -0
  192. package/docs/html/types/TAddressLabel.html +193 -0
  193. package/docs/html/types/TAddressType.html +193 -0
  194. package/docs/html/types/TAddressTypeContent.html +198 -0
  195. package/docs/html/types/TAddressTypes.html +193 -0
  196. package/docs/html/types/TAvailableNetworks.html +193 -0
  197. package/docs/html/types/TConnectToElectrumRes.html +193 -0
  198. package/docs/html/types/TDecodeRawTx.html +214 -0
  199. package/docs/html/types/TElectrumNetworks.html +193 -0
  200. package/docs/html/types/TGetAddressHistory.html +200 -0
  201. package/docs/html/types/TGetByteCountInput.html +193 -0
  202. package/docs/html/types/TGetByteCountInputs.html +193 -0
  203. package/docs/html/types/TGetByteCountOutput.html +193 -0
  204. package/docs/html/types/TGetByteCountOutputs.html +193 -0
  205. package/docs/html/types/TGetData.html +211 -0
  206. package/docs/html/types/TGetTotalFeeObj.html +204 -0
  207. package/docs/html/types/TKeyDerivationAccount.html +193 -0
  208. package/docs/html/types/TKeyDerivationChange.html +193 -0
  209. package/docs/html/types/TKeyDerivationCoinType.html +193 -0
  210. package/docs/html/types/TKeyDerivationIndex.html +193 -0
  211. package/docs/html/types/TKeyDerivationPurpose.html +193 -0
  212. package/docs/html/types/TMessageDataMap.html +210 -0
  213. package/docs/html/types/TMessageKeys.html +193 -0
  214. package/docs/html/types/TOnMessage.html +213 -0
  215. package/docs/html/types/TProcessUnconfirmedTransactions.html +202 -0
  216. package/docs/html/types/TProtocol.html +193 -0
  217. package/docs/html/types/TServer.html +204 -0
  218. package/docs/html/types/TSetData.html +213 -0
  219. package/docs/html/types/TSetupTransactionResponse.html +193 -0
  220. package/docs/html/types/TStorage.html +200 -0
  221. package/docs/html/types/TSubscribedReceive.html +193 -0
  222. package/docs/html/types/TTransactionMessage.html +198 -0
  223. package/docs/html/types/TTxResponse.html +206 -0
  224. package/docs/html/types/TTxResult.html +200 -0
  225. package/docs/html/types/TUnspentAddressScriptHashData.html +198 -0
  226. package/docs/html/types/TWalletDataKeys.html +193 -0
  227. package/docs/html/variables/defaultElectrumPorts.html +193 -0
  228. package/docs/html/variables/electrumConnection.html +199 -0
  229. package/docs/html/variables/mostUsedExchangeTickers.html +241 -0
  230. package/docs/markdown/.nojekyll +1 -0
  231. package/docs/markdown/README.md +1875 -0
  232. package/docs/markdown/classes/Electrum.md +718 -0
  233. package/docs/markdown/classes/Transaction.md +536 -0
  234. package/docs/markdown/classes/Wallet.md +2189 -0
  235. package/docs/markdown/enums/EAddressType.md +41 -0
  236. package/docs/markdown/enums/EAvailableNetworks.md +85 -0
  237. package/docs/markdown/enums/EBoostType.md +30 -0
  238. package/docs/markdown/enums/EFeeId.md +63 -0
  239. package/docs/markdown/enums/EPaymentType.md +30 -0
  240. package/docs/markdown/enums/EProtocol.md +30 -0
  241. package/docs/markdown/enums/EUnit.md +41 -0
  242. package/docs/markdown/interfaces/IAddInput.md +41 -0
  243. package/docs/markdown/interfaces/IAddress.md +69 -0
  244. package/docs/markdown/interfaces/IAddressData.md +41 -0
  245. package/docs/markdown/interfaces/IAddressType.md +7 -0
  246. package/docs/markdown/interfaces/IAddressTypeData.md +74 -0
  247. package/docs/markdown/interfaces/IAddresses.md +7 -0
  248. package/docs/markdown/interfaces/IBoostedTransaction.md +52 -0
  249. package/docs/markdown/interfaces/IBoostedTransactions.md +7 -0
  250. package/docs/markdown/interfaces/ICreateTransaction.md +30 -0
  251. package/docs/markdown/interfaces/ICustomGetAddress.md +41 -0
  252. package/docs/markdown/interfaces/ICustomGetScriptHash.md +30 -0
  253. package/docs/markdown/interfaces/IElectrumGetAddressBalanceRes.md +55 -0
  254. package/docs/markdown/interfaces/IExchangeRates.md +7 -0
  255. package/docs/markdown/interfaces/IFormattedPeerData.md +63 -0
  256. package/docs/markdown/interfaces/IFormattedTransaction.md +217 -0
  257. package/docs/markdown/interfaces/IFormattedTransactions.md +7 -0
  258. package/docs/markdown/interfaces/IGenerateAddresses.md +74 -0
  259. package/docs/markdown/interfaces/IGenerateAddressesResponse.md +30 -0
  260. package/docs/markdown/interfaces/IGetAddress.md +41 -0
  261. package/docs/markdown/interfaces/IGetAddressBalanceRes.md +36 -0
  262. package/docs/markdown/interfaces/IGetAddressByPath.md +30 -0
  263. package/docs/markdown/interfaces/IGetAddressHistoryResponse.md +121 -0
  264. package/docs/markdown/interfaces/IGetAddressResponse.md +41 -0
  265. package/docs/markdown/interfaces/IGetAddressScriptHashesHistoryResponse.md +63 -0
  266. package/docs/markdown/interfaces/IGetDerivationPath.md +100 -0
  267. package/docs/markdown/interfaces/IGetFeeEstimatesResponse.md +52 -0
  268. package/docs/markdown/interfaces/IGetHeaderResponse.md +63 -0
  269. package/docs/markdown/interfaces/IGetNextAvailableAddressResponse.md +52 -0
  270. package/docs/markdown/interfaces/IGetTransactions.md +63 -0
  271. package/docs/markdown/interfaces/IGetTransactionsFromInputs.md +63 -0
  272. package/docs/markdown/interfaces/IGetUtxosResponse.md +30 -0
  273. package/docs/markdown/interfaces/IHeader.md +41 -0
  274. package/docs/markdown/interfaces/IIndexes.md +52 -0
  275. package/docs/markdown/interfaces/IKeyDerivationPath.md +69 -0
  276. package/docs/markdown/interfaces/IKeyDerivationPathData.md +30 -0
  277. package/docs/markdown/interfaces/INewBlock.md +30 -0
  278. package/docs/markdown/interfaces/IOnchainFees.md +63 -0
  279. package/docs/markdown/interfaces/IOutput.md +41 -0
  280. package/docs/markdown/interfaces/IPeerData.md +41 -0
  281. package/docs/markdown/interfaces/IRbfData.md +74 -0
  282. package/docs/markdown/interfaces/ISend.md +30 -0
  283. package/docs/markdown/interfaces/ISendTransaction.md +184 -0
  284. package/docs/markdown/interfaces/ISendTx.md +41 -0
  285. package/docs/markdown/interfaces/ISetupTransaction.md +52 -0
  286. package/docs/markdown/interfaces/ISubscribeToAddress.md +60 -0
  287. package/docs/markdown/interfaces/ISubscribeToHeader.md +59 -0
  288. package/docs/markdown/interfaces/ITargets.md +52 -0
  289. package/docs/markdown/interfaces/ITransaction.md +88 -0
  290. package/docs/markdown/interfaces/ITxHash.md +19 -0
  291. package/docs/markdown/interfaces/ITxHashes.md +55 -0
  292. package/docs/markdown/interfaces/IUtxo.md +112 -0
  293. package/docs/markdown/interfaces/IVin.md +70 -0
  294. package/docs/markdown/interfaces/IVout.md +52 -0
  295. package/docs/markdown/interfaces/IWallet.md +209 -0
  296. package/docs/markdown/interfaces/IWalletData.md +217 -0
  297. package/example/helpers.ts +76 -0
  298. package/example/index.ts +77 -0
  299. package/package.json +69 -0
  300. package/src/electrum/constants.ts +28 -0
  301. package/src/electrum/index.ts +838 -0
  302. package/src/index.ts +6 -0
  303. package/src/shapes/electrum.ts +13 -0
  304. package/src/shapes/index.ts +2 -0
  305. package/src/shapes/wallet.ts +133 -0
  306. package/src/transaction/index.ts +1078 -0
  307. package/src/types/electrum.ts +139 -0
  308. package/src/types/index.ts +3 -0
  309. package/src/types/transaction.ts +58 -0
  310. package/src/types/wallet.ts +503 -0
  311. package/src/utils/conversion.ts +88 -0
  312. package/src/utils/derivation-path.ts +171 -0
  313. package/src/utils/electrum.ts +188 -0
  314. package/src/utils/exchange-rate.ts +70 -0
  315. package/src/utils/helpers.ts +191 -0
  316. package/src/utils/index.ts +7 -0
  317. package/src/utils/result.ts +79 -0
  318. package/src/utils/transaction.ts +417 -0
  319. package/src/utils/wallet.ts +351 -0
  320. package/src/wallet/constants.ts +24 -0
  321. package/src/wallet/index.ts +3304 -0
  322. package/tests/constants.ts +9 -0
  323. package/tests/derivation.test.ts +146 -0
  324. package/tests/electrum.test.ts +137 -0
  325. package/tests/storage.test.ts +222 -0
  326. package/tests/transaction.test.ts +187 -0
  327. package/tests/wallet.test.ts +307 -0
  328. package/tsconfig.json +32 -0
  329. package/typedoc.json +5 -0
@@ -0,0 +1,3304 @@
1
+ import * as bitcoin from 'bitcoinjs-lib';
2
+ import { Network, networks } from 'bitcoinjs-lib';
3
+ import BIP32Factory, { BIP32Interface } from 'bip32';
4
+ import * as ecc from '@bitcoinerlab/secp256k1';
5
+ //import { getExchangeRates } from "../utils/exchange-rate";
6
+ import {
7
+ EAddressType,
8
+ EAvailableNetworks,
9
+ EBoostType,
10
+ EFeeId,
11
+ EPaymentType,
12
+ IAddress,
13
+ IAddresses,
14
+ IBoostedTransaction,
15
+ IBoostedTransactions,
16
+ ICustomGetAddress,
17
+ ICustomGetScriptHash,
18
+ IExchangeRates,
19
+ IFormattedTransaction,
20
+ IFormattedTransactions,
21
+ IGenerateAddresses,
22
+ IGenerateAddressesResponse,
23
+ IGetAddress,
24
+ IGetAddressBalanceRes,
25
+ IGetAddressByPath,
26
+ IGetAddressResponse,
27
+ IGetFeeEstimatesResponse,
28
+ IGetNextAvailableAddressResponse,
29
+ IGetUtxosResponse,
30
+ IHeader,
31
+ IKeyDerivationPath,
32
+ InputData,
33
+ IOnchainFees,
34
+ IOutput,
35
+ IRbfData,
36
+ ISendTransaction,
37
+ ISendTx,
38
+ ISetupTransaction,
39
+ ITransaction,
40
+ ITxHash,
41
+ IUtxo,
42
+ IWallet,
43
+ IWalletData,
44
+ TAddressIndexInfo,
45
+ TAddressTypeContent,
46
+ TAvailableNetworks,
47
+ TGetData,
48
+ TGetTotalFeeObj,
49
+ TOnMessage,
50
+ TProcessUnconfirmedTransactions,
51
+ TServer,
52
+ TSetData,
53
+ TSetupTransactionResponse,
54
+ TTransactionMessage,
55
+ TWalletDataKeys
56
+ } from '../types';
57
+ import {
58
+ decodeOpReturnMessage,
59
+ err,
60
+ formatKeyDerivationPath,
61
+ getAddressTypeFromPath,
62
+ getDataFallback,
63
+ getDefaultWalletData,
64
+ getDefaultWalletDataKeys,
65
+ getExchangeRates,
66
+ getHighestUsedIndexFromTxHashes,
67
+ getKeyDerivationPath,
68
+ getKeyDerivationPathObject,
69
+ getKeyDerivationPathString,
70
+ getScriptHash,
71
+ getSeed,
72
+ getWalletDataStorageKey,
73
+ generateWalletId,
74
+ objectKeys,
75
+ objectsMatch,
76
+ ok,
77
+ Result,
78
+ shuffleArray,
79
+ validateAddress,
80
+ validateMnemonic
81
+ } from '../utils';
82
+ import {
83
+ addressTypes,
84
+ defaultFeesShape,
85
+ getAddressTypeContent
86
+ } from '../shapes';
87
+ import { Electrum } from '../electrum';
88
+ import { Transaction } from '../transaction';
89
+ import { CHUNK_LIMIT, GAP_LIMIT, GENERATE_ADDRESS_AMOUNT } from './constants';
90
+ import cloneDeep from 'lodash.clonedeep';
91
+ import { btcToSats } from '../utils/conversion';
92
+ import * as bip39 from 'bip39';
93
+ import { TLSSocket } from 'tls';
94
+ import { Server } from 'net';
95
+
96
+ const bip32 = BIP32Factory(ecc);
97
+
98
+ export class Wallet {
99
+ private _network: EAvailableNetworks;
100
+ private readonly _mnemonic: string;
101
+ private readonly _passphrase: string;
102
+ private readonly _seed: Buffer;
103
+ private readonly _root: BIP32Interface;
104
+ private _data: IWalletData;
105
+ private _getData: TGetData;
106
+ private _setData?: TSetData;
107
+ private _customGetAddress?: (
108
+ data: ICustomGetAddress
109
+ ) => Promise<Result<IGetAddressResponse>>; // For use with Bitkit.
110
+ private _customGetScriptHash?: (
111
+ data: ICustomGetScriptHash
112
+ ) => Promise<string>; // For use with Bitkit.
113
+ private _pendingRefreshPromises: Array<
114
+ (result: Result<IWalletData>) => void
115
+ > = [];
116
+
117
+ public isRefreshing: boolean;
118
+ public isSwitchingNetworks: boolean;
119
+ public readonly id: string;
120
+ public readonly name: string;
121
+ public electrumOptions?: {
122
+ servers?: TServer | TServer[];
123
+ tls?: TLSSocket;
124
+ net?: Server;
125
+ };
126
+ public electrum: Electrum;
127
+ public exchangeRates: IExchangeRates;
128
+ public onMessage?: TOnMessage;
129
+ public transaction: Transaction;
130
+ public feeEstimates: IOnchainFees;
131
+ public rbf: boolean;
132
+ public selectedFeeId: EFeeId;
133
+ private constructor({
134
+ mnemonic,
135
+ passphrase,
136
+ name,
137
+ network = EAvailableNetworks.mainnet,
138
+ addressType = EAddressType.p2wpkh,
139
+ storage,
140
+ electrumOptions,
141
+ onMessage = (): null => null,
142
+ customGetAddress,
143
+ customGetScriptHash,
144
+ rbf = false,
145
+ selectedFeeId = EFeeId.normal
146
+ }: IWallet) {
147
+ if (!mnemonic) throw new Error('No mnemonic specified.');
148
+ if (!validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic.');
149
+ if (name && name.includes('-'))
150
+ throw new Error('Wallet name cannot include a hyphen (-).');
151
+ this._network = network;
152
+ this._mnemonic = mnemonic;
153
+ this._passphrase = passphrase ?? '';
154
+ this._seed = getSeed(this._mnemonic, this._passphrase);
155
+ this._root = bip32.fromSeed(
156
+ this._seed,
157
+ this.getBitcoinNetwork(this._network)
158
+ );
159
+ this._data = { ...getDefaultWalletData(), addressType };
160
+ this._getData = storage?.getData ?? getDataFallback;
161
+ this._setData = storage?.setData;
162
+ if (customGetAddress) this._customGetAddress = customGetAddress;
163
+ if (customGetScriptHash) this._customGetScriptHash = customGetScriptHash;
164
+ this.id = generateWalletId(this._seed);
165
+ this.name = name ?? this.id;
166
+ this.exchangeRates = {};
167
+ this.transaction = new Transaction({
168
+ wallet: this
169
+ });
170
+ this.feeEstimates = cloneDeep(defaultFeesShape);
171
+ this.onMessage = onMessage;
172
+ this.electrumOptions = electrumOptions;
173
+ this.electrum = new Electrum({
174
+ wallet: this,
175
+ network: this.network,
176
+ servers: electrumOptions?.servers,
177
+ tls: electrumOptions?.tls,
178
+ net: electrumOptions?.net
179
+ });
180
+ this.rbf = rbf;
181
+ this.selectedFeeId = selectedFeeId;
182
+ this.isRefreshing = false;
183
+ this.isSwitchingNetworks = false;
184
+ }
185
+
186
+ public get data(): IWalletData {
187
+ return this._data;
188
+ }
189
+
190
+ public get network(): EAvailableNetworks {
191
+ return this._network;
192
+ }
193
+
194
+ static async create(params: IWallet): Promise<Result<Wallet>> {
195
+ try {
196
+ const wallet = new Wallet(params);
197
+ const res = await wallet.setWalletData();
198
+ if (res.isErr()) return err(res.error.message);
199
+ wallet.updateExchangeRates();
200
+ wallet.updateFeeEstimates();
201
+ console.log('Syncing Wallet...');
202
+ await wallet.refreshWallet({});
203
+ return ok(wallet);
204
+ } catch (e) {
205
+ return err(e);
206
+ }
207
+ }
208
+
209
+ public async switchNetwork(
210
+ network: EAvailableNetworks,
211
+ servers?: TServer | TServer[]
212
+ ): Promise<Result<string>> {
213
+ this.isSwitchingNetworks = true;
214
+ // Disconnect from Electrum.
215
+ await this.electrum.disconnect();
216
+
217
+ this._network = network;
218
+ this._data = getDefaultWalletData();
219
+ const params: IWallet = {
220
+ ...this,
221
+ mnemonic: this._mnemonic,
222
+ passphrase: this._passphrase,
223
+ network,
224
+ electrumOptions: {
225
+ servers,
226
+ tls: this.electrumOptions?.tls,
227
+ net: this.electrumOptions?.net
228
+ },
229
+ storage: {
230
+ getData: this._getData,
231
+ setData: this._setData
232
+ },
233
+ data: getDefaultWalletData()
234
+ };
235
+ const createRes = await Wallet.create(params);
236
+ if (createRes.isErr()) return err(createRes.error.message);
237
+ Object.assign(this, createRes.value);
238
+ await this.updateFeeEstimates(true);
239
+ this.isSwitchingNetworks = false;
240
+ return ok(`Successfully switched to ${network}.`);
241
+ }
242
+
243
+ /**
244
+ * Updates the address type for the current wallet.
245
+ * @param {EAddressType} addressType
246
+ * @returns {Promise<void>}
247
+ */
248
+ async updateAddressType(addressType: EAddressType): Promise<void> {
249
+ this._data.addressType = addressType;
250
+ await this.saveWalletData('addressType', addressType);
251
+ await this.refreshWallet({});
252
+ }
253
+
254
+ /**
255
+ * Refreshes/Syncs the wallet data.
256
+ * @param {boolean} scanAllAddresses
257
+ * @param {boolean} updateAllAddressTypes
258
+ * @returns {Promise<Result<IWalletData>>}
259
+ */
260
+ public async refreshWallet({
261
+ scanAllAddresses = false,
262
+ updateAllAddressTypes = true
263
+ } = {}): Promise<Result<IWalletData>> {
264
+ if (this.isRefreshing) {
265
+ return new Promise((resolve) => {
266
+ this._pendingRefreshPromises.push(resolve);
267
+ });
268
+ }
269
+ this.isRefreshing = true;
270
+ try {
271
+ await this.setZeroIndexAddresses();
272
+ const addressType: undefined | EAddressType = updateAllAddressTypes
273
+ ? undefined
274
+ : this.data.addressType;
275
+ const r1 = await this.updateAddressIndexes({ addressType });
276
+ if (r1.isErr()) {
277
+ return this._handleRefreshError(r1.error.message);
278
+ }
279
+ const r2 = await this.getUtxos({ scanAllAddresses });
280
+ if (r2.isErr()) {
281
+ return this._handleRefreshError(r2.error.message);
282
+ }
283
+ const r3 = await this.updateTransactions({ scanAllAddresses });
284
+ if (r3.isErr()) {
285
+ return this._handleRefreshError(r3.error.message);
286
+ }
287
+ await this.electrum.subscribeToAddresses();
288
+ this._resolveAllPendingRefreshPromises(ok(this.data));
289
+ return ok(this.data);
290
+ } catch (e) {
291
+ return this._handleRefreshError(e);
292
+ }
293
+ }
294
+
295
+ private _resolveAllPendingRefreshPromises(result: Result<IWalletData>): void {
296
+ this.isRefreshing = false;
297
+ while (this._pendingRefreshPromises.length > 0) {
298
+ const resolve = this._pendingRefreshPromises.shift();
299
+ if (resolve) {
300
+ resolve(result);
301
+ }
302
+ }
303
+ }
304
+
305
+ private _handleRefreshError(errorMessage: string): Result<IWalletData> {
306
+ this.isRefreshing = false;
307
+ this._resolveAllPendingRefreshPromises(err(errorMessage));
308
+ return err(errorMessage);
309
+ }
310
+
311
+ /**
312
+ * Sets the wallet data object.
313
+ * @returns {Promise<Result<boolean>>}
314
+ * @private
315
+ */
316
+ private async setWalletData(): Promise<Result<boolean>> {
317
+ try {
318
+ const storageIdCheckRes = await this.storageIdCheck(this.id);
319
+ if (storageIdCheckRes.isErr())
320
+ return err(storageIdCheckRes.error.message);
321
+ this._data = getDefaultWalletData();
322
+ const walletDataResponse = await this.getWalletData();
323
+ if (walletDataResponse.isErr())
324
+ return err(walletDataResponse.error.message);
325
+ this._data = walletDataResponse.value;
326
+ return ok(true);
327
+ } catch (e) {
328
+ return err(e);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Ensure we are not overwriting wallet data of a different wallet by checking that the wallet id's match.
334
+ * @private
335
+ * @async
336
+ * @param {string} id
337
+ * @returns {Promise<Result<string>>}
338
+ */
339
+ private async storageIdCheck(id: string): Promise<Result<string>> {
340
+ const storageKey = this.getWalletDataKey('id');
341
+ const res = await this._getData(storageKey);
342
+ // No id found, it is safe to save to storage.
343
+ if (res.isErr() || !res.value) {
344
+ // Save id to storage.
345
+ this._data.id = id;
346
+ await this.saveWalletData('id', id);
347
+ return ok('Saved ID to storage.');
348
+ }
349
+ // If the ID saved in storage does not match return an error and notify the developer.
350
+ if (res.value !== id) {
351
+ const msg =
352
+ 'Mismatched id found in storage. Change the wallet name or delete the old wallet from storage and try again.';
353
+ console.log(msg);
354
+ return err(msg);
355
+ }
356
+ return ok("ID's match, it's safe to continue.");
357
+ }
358
+
359
+ /**
360
+ * Returns the key used for storing wallet data in the key/value pair.
361
+ * @returns {string}
362
+ * @param key
363
+ */
364
+ public getWalletDataKey(key: keyof IWalletData): string {
365
+ return getWalletDataStorageKey(this.name, this._network, key);
366
+ }
367
+
368
+ /**
369
+ * Gets the wallet data object from storage if able.
370
+ * Otherwise, it falls back to the default wallet data object.
371
+ * @returns {Promise<Result<IWalletData>>}
372
+ */
373
+ public async getWalletData(): Promise<Result<IWalletData>> {
374
+ try {
375
+ const walletDataKeys = getDefaultWalletDataKeys();
376
+ const walletData: IWalletData = getDefaultWalletData();
377
+ for (const key of walletDataKeys) {
378
+ let dataResult;
379
+ try {
380
+ const walletDataKey = this.getWalletDataKey(key);
381
+ const getDataRes = await this._getData(walletDataKey);
382
+ if (getDataRes.isErr()) {
383
+ //dataResult = getDataRes?.value ?? walletData[key];
384
+ return err(dataResult.error.message);
385
+ }
386
+ dataResult = getDataRes?.value;
387
+ } catch (e) {
388
+ console.log(e);
389
+ }
390
+ const data = dataResult ?? walletData[key];
391
+ switch (key) {
392
+ case 'id':
393
+ walletData[key] = data as string;
394
+ break;
395
+ case 'addressType':
396
+ walletData[key] = data as EAddressType;
397
+ break;
398
+ case 'addresses':
399
+ case 'changeAddresses':
400
+ walletData[key] = data as TAddressTypeContent<IAddresses>;
401
+ break;
402
+ case 'addressIndex':
403
+ case 'changeAddressIndex':
404
+ walletData[key] = data as TAddressTypeContent<IAddress>;
405
+ break;
406
+ case 'lastUsedAddressIndex':
407
+ case 'lastUsedChangeAddressIndex':
408
+ walletData[key] = data as TAddressTypeContent<IAddress>;
409
+ break;
410
+ case 'utxos':
411
+ walletData[key] = data as IUtxo[];
412
+ break;
413
+ case 'blacklistedUtxos':
414
+ walletData[key] = data as IUtxo[];
415
+ break;
416
+ case 'unconfirmedTransactions':
417
+ case 'transactions':
418
+ walletData[key] = data as IFormattedTransactions;
419
+ break;
420
+ case 'transaction':
421
+ walletData[key] = data as ISendTransaction;
422
+ break;
423
+ case 'balance':
424
+ walletData[key] = data as number;
425
+ break;
426
+ case 'header':
427
+ walletData[key] = data as IHeader;
428
+ break;
429
+ case 'boostedTransactions':
430
+ walletData[key] = data as IBoostedTransactions;
431
+ break;
432
+ case 'selectedFeeId':
433
+ walletData[key] = data as EFeeId;
434
+ break;
435
+ case 'exchangeRates':
436
+ walletData[key] = data as IExchangeRates;
437
+ break;
438
+ case 'feeEstimates':
439
+ walletData[key] = data as IOnchainFees;
440
+ break;
441
+ default:
442
+ return err(`Unhandled key in getWalletData: ${key}`);
443
+ }
444
+ }
445
+ return ok(walletData);
446
+ } catch (e) {
447
+ console.log(e);
448
+ return err(e);
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Returns the Network object of the currently selected network (bitcoin or testnet).
454
+ * @param {TAvailableNetworks} [network]
455
+ * @returns {Network}
456
+ */
457
+ private getBitcoinNetwork(network?: TAvailableNetworks): Network {
458
+ if (!network) network = this._network;
459
+ return bitcoin.networks[network];
460
+ }
461
+
462
+ /**
463
+ * Ensures the provided mnemonic matches the one stored in the wallet and is valid.
464
+ * @param mnemonic
465
+ * @returns {boolean}
466
+ */
467
+ isValid(mnemonic): boolean {
468
+ return mnemonic === this._mnemonic && validateMnemonic(mnemonic);
469
+ }
470
+
471
+ /**
472
+ * Returns the address for the specified path and address type.
473
+ * @param {string} path
474
+ * @param {EAddressType} addressType
475
+ * @returns {IGetAddressResponse}
476
+ * @private
477
+ */
478
+ private async _getAddress(
479
+ path: string,
480
+ addressType: EAddressType
481
+ ): Promise<IGetAddressResponse> {
482
+ if (this._customGetAddress) {
483
+ const data = {
484
+ path,
485
+ type: addressType,
486
+ selectedNetwork: this.electrum.getElectrumNetwork(this._network)
487
+ };
488
+ const res = await this._customGetAddress(data);
489
+ if (res.isErr()) {
490
+ return {
491
+ address: '',
492
+ path,
493
+ publicKey: ''
494
+ };
495
+ }
496
+ return res.value;
497
+ }
498
+ const keyPair = this._root.derivePath(path);
499
+ let address;
500
+ switch (addressType) {
501
+ case EAddressType.p2wpkh:
502
+ //Get Bech32 (bc1) address
503
+ address =
504
+ bitcoin.payments.p2wpkh({
505
+ pubkey: keyPair.publicKey,
506
+ network: this.getBitcoinNetwork()
507
+ }).address ?? '';
508
+ break;
509
+ case EAddressType.p2sh:
510
+ //Get Segwit P2SH Address (3)
511
+ address =
512
+ bitcoin.payments.p2sh({
513
+ redeem: bitcoin.payments.p2wpkh({
514
+ pubkey: keyPair.publicKey,
515
+ network: this.getBitcoinNetwork()
516
+ }),
517
+ network: this.getBitcoinNetwork()
518
+ }).address ?? '';
519
+ break;
520
+ //Get Legacy Address (1)
521
+ case EAddressType.p2pkh:
522
+ address =
523
+ bitcoin.payments.p2pkh({
524
+ pubkey: keyPair.publicKey,
525
+ network: this.getBitcoinNetwork()
526
+ }).address ?? '';
527
+ break;
528
+ }
529
+ return {
530
+ address,
531
+ path,
532
+ publicKey: keyPair.publicKey.toString('hex')
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Returns a single Bitcoin address based on the provided address type,
538
+ * index and whether it is a change address.
539
+ * @param {TKeyDerivationIndex} [index]
540
+ * @param {boolean} [changeAddress]
541
+ * @param {EAddressType} [addressType]
542
+ * @returns {string}
543
+ */
544
+ public async getAddress({
545
+ index,
546
+ changeAddress = false,
547
+ addressType = this.data.addressType
548
+ }: IGetAddress = {}): Promise<string> {
549
+ try {
550
+ if (index === undefined) {
551
+ const addressIndex = this.data.addressIndex[addressType];
552
+ index = addressIndex.index >= 0 ? String(addressIndex.index) : '0';
553
+ }
554
+ const pathRes = getKeyDerivationPathString({
555
+ addressType,
556
+ changeAddress,
557
+ index,
558
+ network: this._network
559
+ });
560
+ if (pathRes.isErr()) {
561
+ return '';
562
+ }
563
+ const path = pathRes.value;
564
+ const res = await this._getAddress(path, addressType);
565
+ if (!res?.address) return '';
566
+ return res.address;
567
+ } catch {
568
+ return '';
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Get address for a given keyPair, network and type.
574
+ * @param {string} path
575
+ * @param {EAddressType} addressType
576
+ * @returns {Promise<Result<string>>}
577
+ */
578
+ public async getAddressByPath({
579
+ path,
580
+ addressType
581
+ }: IGetAddressByPath): Promise<Result<IGetAddressResponse>> {
582
+ if (!path) {
583
+ return err('No path specified');
584
+ }
585
+ if (!addressType) {
586
+ const res = getAddressTypeFromPath(path);
587
+ if (res.isErr()) return err(res.error.message);
588
+ addressType = res.value;
589
+ }
590
+ try {
591
+ const getAddressRes = await this._getAddress(path, addressType);
592
+ if (!getAddressRes?.address) return err('Unable to get address.');
593
+ return ok(getAddressRes);
594
+ } catch (e) {
595
+ return err(e);
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Attempts to connect to the specified Electrum server(s).
601
+ * @param {TServer | TServer[]} servers
602
+ * @returns {Promise<Result<string>>}
603
+ */
604
+ public async connectToElectrum(
605
+ servers?: TServer | TServer[]
606
+ ): Promise<Result<string>> {
607
+ const res = await this.electrum.connectToElectrum({
608
+ network: this._network,
609
+ servers: servers ?? this.electrumOptions?.servers
610
+ });
611
+ let msg = 'Unable to connect to Electrum server.';
612
+ if (res.isErr()) {
613
+ console.log(msg);
614
+ return err(msg);
615
+ }
616
+ msg = 'Connected to Electrum server.';
617
+ return ok(msg);
618
+ }
619
+
620
+ /**
621
+ * Returns the address balance for the specified address.
622
+ * @param {string} address
623
+ * @returns {Promise<Result<IGetAddressBalanceRes>>}
624
+ */
625
+ public async getAddressBalance(
626
+ address: string
627
+ ): Promise<Result<IGetAddressBalanceRes>> {
628
+ const scriptHash = await this.getScriptHash({
629
+ address,
630
+ network: this._network
631
+ });
632
+ const res = await this.electrum.getAddressBalance(scriptHash);
633
+ if (res.error) return err('Unable to get address balance at this time.');
634
+ return ok({ unconfirmed: res.unconfirmed, confirmed: res.confirmed });
635
+ }
636
+
637
+ /**
638
+ * Returns combined balance of provided addresses.
639
+ * @async
640
+ * @param {string[]} addresses
641
+ * @returns {Promise<Result<number>>}
642
+ */
643
+ public async getAddressesBalance(
644
+ addresses: string[] = []
645
+ ): Promise<Result<number>> {
646
+ try {
647
+ const network = this._network;
648
+ const scriptHashes = await Promise.all(
649
+ addresses.map(async (address) => {
650
+ return await this.getScriptHash({ address, network });
651
+ })
652
+ );
653
+ const res =
654
+ await this.electrum.getAddressScriptHashBalances(scriptHashes);
655
+ if (res.error) {
656
+ return err(res.data);
657
+ }
658
+ return ok(
659
+ res.data.reduce((acc, cur) => {
660
+ return (
661
+ acc +
662
+ Number(cur.result?.confirmed ?? 0) +
663
+ Number(cur.result?.unconfirmed ?? 0)
664
+ );
665
+ }, 0) || 0
666
+ );
667
+ } catch (e) {
668
+ return err(e);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Get scriptHash for a given address
674
+ * @param {string} address
675
+ * @param {EAvailableNetworks} network
676
+ * @returns {Promise<string>}
677
+ */
678
+ public async getScriptHash({
679
+ address,
680
+ network
681
+ }: {
682
+ address: string;
683
+ network: EAvailableNetworks;
684
+ }): Promise<string> {
685
+ if (this._customGetScriptHash) {
686
+ const selectedNetwork = this.electrum.getElectrumNetwork(network);
687
+ return await this._customGetScriptHash({ address, selectedNetwork });
688
+ }
689
+ return getScriptHash({ address, network });
690
+ }
691
+
692
+ /**
693
+ * Returns private key for the provided path.
694
+ * @param path
695
+ * @returns {string}
696
+ */
697
+ getPrivateKey(path: string): string {
698
+ const keyPair = this._root.derivePath(path);
699
+ return keyPair.toWIF();
700
+ }
701
+
702
+ /**
703
+ * Returns the balance for the specified scriptHash.
704
+ * @param {string} scriptHash
705
+ * @returns {Promise<Result<IGetAddressBalanceRes>>}
706
+ */
707
+ public async getScriptHashBalance(
708
+ scriptHash: string
709
+ ): Promise<Result<IGetAddressBalanceRes>> {
710
+ const res = await this.electrum.getAddressBalance(scriptHash);
711
+ if (res.error) return err('Unable to get address balance at this time.');
712
+ return ok({ unconfirmed: res.unconfirmed, confirmed: res.confirmed });
713
+ }
714
+
715
+ /**
716
+ * Returns the known balance from storage.
717
+ * @returns {number}
718
+ */
719
+ public getBalance(): number {
720
+ return this?.data?.balance ?? 0;
721
+ }
722
+
723
+ /**
724
+ * Generates a series of addresses based on the specified params.
725
+ * @async
726
+ * @param {string} selectedWallet - Wallet ID
727
+ * @param {number} [addressAmount] - Number of addresses to generate.
728
+ * @param {number} [changeAddressAmount] - Number of changeAddresses to generate.
729
+ * @param {number} [addressIndex] - What index to start generating addresses at.
730
+ * @param {number} [changeAddressIndex] - What index to start generating changeAddresses at.
731
+ * @param {string} [keyDerivationPath] - The path to generate addresses from.
732
+ * @param {string} [addressType] - Determines what type of address to generate (p2pkh, p2sh, p2wpkh).
733
+ * @returns {Promise<Result<IGenerateAddressesResponse>>}
734
+ */
735
+ public async generateAddresses({
736
+ addressAmount = 10,
737
+ changeAddressAmount = 10,
738
+ addressIndex = 0,
739
+ changeAddressIndex = 0,
740
+ keyDerivationPath,
741
+ addressType = this.data.addressType
742
+ }: IGenerateAddresses): Promise<Result<IGenerateAddressesResponse>> {
743
+ const network = this._network;
744
+ try {
745
+ if (!keyDerivationPath) {
746
+ // Set derivation path accordingly based on address type.
747
+ const keyDerivationPathResponse = getKeyDerivationPath({
748
+ network,
749
+ addressType
750
+ });
751
+ if (keyDerivationPathResponse.isErr())
752
+ return err(keyDerivationPathResponse.error.message);
753
+ keyDerivationPath = keyDerivationPathResponse.value;
754
+ }
755
+
756
+ const addresses = {} as IAddresses;
757
+ const changeAddresses = {} as IAddresses;
758
+ const addressArray = new Array(addressAmount).fill(null);
759
+ const changeAddressArray = new Array(changeAddressAmount).fill(null);
760
+
761
+ await Promise.all(
762
+ addressArray.map(async (_item, i) => {
763
+ const index = i + addressIndex;
764
+ const path = { ...keyDerivationPath };
765
+ path.index = `${index}`;
766
+ const addressPath = formatKeyDerivationPath({
767
+ path,
768
+ network,
769
+ changeAddress: false,
770
+ index: `${index}`
771
+ });
772
+ if (addressPath.isErr()) {
773
+ throw addressPath.error;
774
+ }
775
+ const address = await this.getAddressByPath({
776
+ path: addressPath.value.pathString
777
+ });
778
+ if (address.isErr()) {
779
+ throw address.error;
780
+ }
781
+ const scriptHash = await this.getScriptHash({
782
+ address: address.value.address,
783
+ network
784
+ });
785
+ if (!scriptHash) {
786
+ throw new Error('Unable to get script hash.');
787
+ }
788
+ addresses[scriptHash] = {
789
+ ...address.value,
790
+ index,
791
+ scriptHash
792
+ };
793
+ })
794
+ );
795
+
796
+ await Promise.all(
797
+ changeAddressArray.map(async (_item, i) => {
798
+ const index = i + changeAddressIndex;
799
+ const path = { ...keyDerivationPath };
800
+ path.index = `${index}`;
801
+ const changeAddressPath = formatKeyDerivationPath({
802
+ path,
803
+ network,
804
+ changeAddress: true,
805
+ index: `${index}`
806
+ });
807
+ if (changeAddressPath.isErr()) {
808
+ throw changeAddressPath.error;
809
+ }
810
+
811
+ const address = await this.getAddressByPath({
812
+ path: changeAddressPath.value.pathString
813
+ });
814
+ if (address.isErr()) {
815
+ throw address.error;
816
+ }
817
+ const scriptHash = await this.getScriptHash({
818
+ address: address.value.address,
819
+ network
820
+ });
821
+ if (!scriptHash) {
822
+ throw new Error('Unable to get script hash.');
823
+ }
824
+ changeAddresses[scriptHash] = {
825
+ ...address.value,
826
+ index,
827
+ scriptHash
828
+ };
829
+ })
830
+ );
831
+
832
+ return ok({ addresses, changeAddresses });
833
+ } catch (e) {
834
+ return err(e);
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Ensures the connection to Electrum is still available.
840
+ * Will attempt to reconnect if not initially available.
841
+ * @returns {Promise<Result<string>>}
842
+ */
843
+ public async checkElectrumConnection(): Promise<Result<string>> {
844
+ const isConnected = this.electrum.connectedToElectrum;
845
+ if (!isConnected) {
846
+ return await this.connectToElectrum();
847
+ }
848
+ // Ensure we're connected by pinging the server.
849
+ const pingRes = await this.electrum.isConnected();
850
+ if (pingRes) return ok('Is connected to Electrum.Z');
851
+ return err('Failed Electrum connection check.');
852
+ }
853
+
854
+ /**
855
+ * Returns the next available address for the given addresstype.
856
+ * @param {EAddressType} addressType
857
+ * @returns {Promise<Result<IGetNextAvailableAddressResponse>>}
858
+ */
859
+ public async getNextAvailableAddress(
860
+ addressType?: EAddressType
861
+ ): Promise<Result<IGetNextAvailableAddressResponse>> {
862
+ const checkRes = await this.checkElectrumConnection();
863
+ if (checkRes.isErr()) return err(checkRes.error.message);
864
+ try {
865
+ const network = this._network;
866
+ addressType = addressType ?? this.data.addressType;
867
+ const currentWallet = this.data;
868
+ const { path } = addressTypes[addressType]; // Assuming addressTypes is globally defined.
869
+ const result = formatKeyDerivationPath({ path, network });
870
+
871
+ if (result.isErr()) {
872
+ return err(result.error.message);
873
+ }
874
+
875
+ const { pathObject: keyDerivationPath } = result.value;
876
+
877
+ //The currently known/stored address index.
878
+ let addressIndex = currentWallet.addressIndex[addressType];
879
+ let lastUsedAddressIndex =
880
+ currentWallet.lastUsedAddressIndex[addressType];
881
+ let changeAddressIndex = currentWallet.changeAddressIndex[addressType];
882
+ let lastUsedChangeAddressIndex =
883
+ currentWallet.lastUsedChangeAddressIndex[addressType];
884
+
885
+ if (addressIndex?.index < 0 || !addressIndex?.address) {
886
+ const generatedAddresses = await this.generateAddresses({
887
+ addressAmount: GENERATE_ADDRESS_AMOUNT,
888
+ addressIndex: 0,
889
+ changeAddressAmount: 0,
890
+ keyDerivationPath,
891
+ addressType
892
+ });
893
+ if (generatedAddresses.isErr()) {
894
+ return err(generatedAddresses.error);
895
+ }
896
+ const addresses = generatedAddresses.value.addresses;
897
+ const sorted = Object.values(addresses).sort(
898
+ (a, b) => a.index - b.index
899
+ );
900
+ addressIndex = sorted[0];
901
+ }
902
+
903
+ if (changeAddressIndex?.index < 0 || !changeAddressIndex?.address) {
904
+ const generatedAddresses = await this.generateAddresses({
905
+ addressAmount: 0,
906
+ changeAddressAmount: GENERATE_ADDRESS_AMOUNT,
907
+ changeAddressIndex: 0,
908
+ keyDerivationPath,
909
+ addressType
910
+ });
911
+ if (generatedAddresses.isErr()) {
912
+ return err(generatedAddresses.error);
913
+ }
914
+ const addresses = generatedAddresses.value.changeAddresses;
915
+ const sorted = Object.values(addresses).sort(
916
+ (a, b) => a.index - b.index
917
+ );
918
+ changeAddressIndex = sorted[0];
919
+ }
920
+
921
+ let addresses: IAddresses = currentWallet.addresses[addressType];
922
+ let changeAddresses: IAddresses =
923
+ currentWallet.changeAddresses[addressType];
924
+
925
+ //How many addresses/changeAddresses are currently stored
926
+ const addressCount = Object.values(addresses).length;
927
+ const changeAddressCount = Object.values(changeAddresses).length;
928
+
929
+ /*
930
+ * Create more addresses if less than the default GENERATE_ADDRESS_AMOUNT or the highest address index matches the current address count
931
+ */
932
+
933
+ if (
934
+ addressCount < GENERATE_ADDRESS_AMOUNT ||
935
+ addressIndex.index === addressCount
936
+ ) {
937
+ const newAddresses = await this.addAddresses({
938
+ addressAmount: GENERATE_ADDRESS_AMOUNT,
939
+ changeAddressAmount: 0,
940
+ addressIndex: addressIndex.index,
941
+ changeAddressIndex: 0,
942
+ keyDerivationPath,
943
+ addressType
944
+ });
945
+ if (newAddresses.isOk()) {
946
+ addresses = newAddresses.value.addresses;
947
+ }
948
+ }
949
+
950
+ /*
951
+ * Create more change addresses if none exist or the highest change address index matches the current
952
+ * change address count
953
+ */
954
+ if (
955
+ changeAddressCount < GENERATE_ADDRESS_AMOUNT ||
956
+ changeAddressIndex.index === changeAddressCount
957
+ ) {
958
+ const newChangeAddresses = await this.addAddresses({
959
+ addressAmount: 0,
960
+ changeAddressAmount: GENERATE_ADDRESS_AMOUNT,
961
+ addressIndex: 0,
962
+ changeAddressIndex: changeAddressIndex.index,
963
+ keyDerivationPath,
964
+ addressType
965
+ });
966
+ if (newChangeAddresses.isOk()) {
967
+ changeAddresses = newChangeAddresses.value.changeAddresses;
968
+ }
969
+ }
970
+
971
+ //Store all addresses that are to be searched and used in this method.
972
+ let allAddresses = Object.values(addresses).filter(
973
+ ({ index }) => index >= addressIndex.index
974
+ );
975
+
976
+ let addressesToScan = allAddresses;
977
+
978
+ //Store all change addresses that are to be searched and used in this method.
979
+ let allChangeAddresses = Object.values(changeAddresses).filter(
980
+ ({ index }) => index >= changeAddressIndex.index
981
+ );
982
+ let changeAddressesToScan = allChangeAddresses;
983
+
984
+ //Prep for batch request
985
+ let combinedAddressesToScan = [
986
+ ...addressesToScan,
987
+ ...changeAddressesToScan
988
+ ];
989
+
990
+ let foundLastUsedAddress = false;
991
+ let foundLastUsedChangeAddress = false;
992
+ let addressHasBeenUsed = false;
993
+ let changeAddressHasBeenUsed = false;
994
+
995
+ // If an error occurs, return last known/available indexes.
996
+ const lastKnownIndexes = ok({
997
+ addressIndex,
998
+ lastUsedAddressIndex,
999
+ changeAddressIndex,
1000
+ lastUsedChangeAddressIndex
1001
+ });
1002
+
1003
+ while (!foundLastUsedAddress || !foundLastUsedChangeAddress) {
1004
+ //Check if transactions are pending in the mempool.
1005
+ const addressHistory = await this.electrum.getAddressHistory({
1006
+ scriptHashes: combinedAddressesToScan
1007
+ });
1008
+
1009
+ if (addressHistory.isErr()) {
1010
+ console.log(addressHistory.error.message);
1011
+ return lastKnownIndexes;
1012
+ }
1013
+
1014
+ const txHashes = addressHistory.value;
1015
+
1016
+ const highestUsedIndex = getHighestUsedIndexFromTxHashes({
1017
+ txHashes,
1018
+ addresses,
1019
+ changeAddresses,
1020
+ addressIndex,
1021
+ changeAddressIndex
1022
+ });
1023
+
1024
+ if (highestUsedIndex.isErr()) {
1025
+ console.log(highestUsedIndex.error.message);
1026
+ return lastKnownIndexes;
1027
+ }
1028
+
1029
+ addressIndex = highestUsedIndex.value.addressIndex;
1030
+ changeAddressIndex = highestUsedIndex.value.changeAddressIndex;
1031
+ if (highestUsedIndex.value.foundAddressIndex) {
1032
+ addressHasBeenUsed = true;
1033
+ }
1034
+ if (highestUsedIndex.value.foundChangeAddressIndex) {
1035
+ changeAddressHasBeenUsed = true;
1036
+ }
1037
+
1038
+ const highestStoredIndex = this.getHighestStoredAddressIndex({
1039
+ addressType
1040
+ });
1041
+
1042
+ if (highestStoredIndex.isErr()) {
1043
+ console.log(highestStoredIndex.error.message);
1044
+ return lastKnownIndexes;
1045
+ }
1046
+
1047
+ const {
1048
+ addressIndex: highestUsedAddressIndex,
1049
+ changeAddressIndex: highestUsedChangeAddressIndex
1050
+ } = highestUsedIndex.value;
1051
+ const {
1052
+ addressIndex: highestStoredAddressIndex,
1053
+ changeAddressIndex: highestStoredChangeAddressIndex
1054
+ } = highestStoredIndex.value;
1055
+
1056
+ if (highestUsedAddressIndex.index < highestStoredAddressIndex.index) {
1057
+ foundLastUsedAddress = true;
1058
+ }
1059
+
1060
+ if (
1061
+ highestUsedChangeAddressIndex.index <
1062
+ highestStoredChangeAddressIndex.index
1063
+ ) {
1064
+ foundLastUsedChangeAddress = true;
1065
+ }
1066
+
1067
+ if (foundLastUsedAddress && foundLastUsedChangeAddress) {
1068
+ //Increase index by one if the current index was found in a txHash or is greater than the previous index.
1069
+ let newAddressIndex = addressIndex.index;
1070
+ if (
1071
+ highestUsedAddressIndex.index > addressIndex.index ||
1072
+ addressHasBeenUsed
1073
+ ) {
1074
+ const index = highestUsedAddressIndex.index;
1075
+ if (highestUsedAddressIndex && index >= 0) {
1076
+ lastUsedAddressIndex = highestUsedAddressIndex;
1077
+ }
1078
+ newAddressIndex = index >= 0 ? index + 1 : index;
1079
+ }
1080
+
1081
+ let newChangeAddressIndex = changeAddressIndex.index;
1082
+ if (
1083
+ highestUsedChangeAddressIndex.index > changeAddressIndex.index ||
1084
+ changeAddressHasBeenUsed
1085
+ ) {
1086
+ const index = highestUsedChangeAddressIndex.index;
1087
+ if (highestUsedChangeAddressIndex && index >= 0) {
1088
+ lastUsedChangeAddressIndex = highestUsedChangeAddressIndex;
1089
+ }
1090
+ newChangeAddressIndex = index >= 0 ? index + 1 : index;
1091
+ }
1092
+
1093
+ //Find and return the new address index.
1094
+ const nextAvailableAddress = Object.values(allAddresses).find(
1095
+ ({ index }) => index === newAddressIndex
1096
+ );
1097
+ //Find and return the new change address index.
1098
+ const nextAvailableChangeAddress = Object.values(
1099
+ allChangeAddresses
1100
+ ).find(({ index }) => index === newChangeAddressIndex);
1101
+ if (!nextAvailableAddress || !nextAvailableChangeAddress) {
1102
+ return lastKnownIndexes;
1103
+ }
1104
+ return ok({
1105
+ addressIndex: nextAvailableAddress,
1106
+ lastUsedAddressIndex,
1107
+ changeAddressIndex: nextAvailableChangeAddress,
1108
+ lastUsedChangeAddressIndex
1109
+ });
1110
+ }
1111
+
1112
+ //Create receiving addresses for the next round
1113
+ if (!foundLastUsedAddress) {
1114
+ const newAddresses = await this.addAddresses({
1115
+ addressAmount: GENERATE_ADDRESS_AMOUNT,
1116
+ changeAddressAmount: 0,
1117
+ addressIndex: highestStoredIndex.value.addressIndex.index,
1118
+ changeAddressIndex: 0,
1119
+ keyDerivationPath,
1120
+ addressType
1121
+ });
1122
+ if (newAddresses.isOk()) {
1123
+ addresses = newAddresses.value.addresses || {};
1124
+ }
1125
+ }
1126
+ //Create change addresses for the next round
1127
+ if (!foundLastUsedChangeAddress) {
1128
+ const newChangeAddresses = await this.addAddresses({
1129
+ addressAmount: 0,
1130
+ changeAddressAmount: GENERATE_ADDRESS_AMOUNT,
1131
+ addressIndex: 0,
1132
+ changeAddressIndex:
1133
+ highestStoredIndex.value.changeAddressIndex.index,
1134
+ keyDerivationPath,
1135
+ addressType
1136
+ });
1137
+ if (newChangeAddresses.isOk()) {
1138
+ changeAddresses = newChangeAddresses.value.changeAddresses || {};
1139
+ }
1140
+ }
1141
+
1142
+ // Store newly created addresses to scan in the next round.
1143
+ addressesToScan = Object.values(addresses);
1144
+ changeAddressesToScan = Object.values(changeAddresses);
1145
+ combinedAddressesToScan = [
1146
+ ...addressesToScan,
1147
+ ...changeAddressesToScan
1148
+ ];
1149
+ // Store the newly created addresses used for this method.
1150
+ allAddresses = [...allAddresses, ...addressesToScan];
1151
+ allChangeAddresses = [...allChangeAddresses, ...changeAddressesToScan];
1152
+ }
1153
+
1154
+ return lastKnownIndexes;
1155
+ } catch (e) {
1156
+ return err(e);
1157
+ }
1158
+ }
1159
+
1160
+ /**
1161
+ * Returns the highest address and change address index stored in the app for the specified wallet and network.
1162
+ * Retrives the highest stored address index for the provided address type.
1163
+ * @param {EAddressType} addressType
1164
+ * @returns {Result<{ addressIndex: IAddress; changeAddressIndex: IAddress }>}
1165
+ */
1166
+ public getHighestStoredAddressIndex({
1167
+ addressType
1168
+ }: {
1169
+ addressType: EAddressType;
1170
+ }): Result<{
1171
+ addressIndex: IAddress;
1172
+ changeAddressIndex: IAddress;
1173
+ }> {
1174
+ try {
1175
+ const currentWallet = this.data;
1176
+ const addresses = currentWallet.addresses[addressType];
1177
+ const changeAddresses = currentWallet.changeAddresses[addressType];
1178
+
1179
+ const addressIndex = Object.values(addresses).reduce((prev, current) => {
1180
+ return prev.index > current.index ? prev : current;
1181
+ });
1182
+
1183
+ const changeAddressIndex = Object.values(changeAddresses).reduce(
1184
+ (prev, current) => (prev.index > current.index ? prev : current)
1185
+ );
1186
+
1187
+ return ok({ addressIndex, changeAddressIndex });
1188
+ } catch (e) {
1189
+ return err(e);
1190
+ }
1191
+ }
1192
+
1193
+ /**
1194
+ * This method will generate addresses as specified and return an object of filtered addresses to ensure no duplicates are returned.
1195
+ * @async
1196
+ * @private
1197
+ * @param {number} [addressAmount]
1198
+ * @param {number} [changeAddressAmount]
1199
+ * @param {number} [addressIndex]
1200
+ * @param {number} [changeAddressIndex]
1201
+ * @param {IKeyDerivationPath} [keyDerivationPath]
1202
+ * @param {EAddressType} [addressType]
1203
+ * @returns {Promise<Result<IGenerateAddressesResponse>>}
1204
+ */
1205
+ private async addAddresses({
1206
+ addressAmount = 5,
1207
+ changeAddressAmount = 5,
1208
+ addressIndex = 0,
1209
+ changeAddressIndex = 0,
1210
+ addressType,
1211
+ keyDerivationPath
1212
+ }: IGenerateAddresses): Promise<Result<IGenerateAddressesResponse>> {
1213
+ if (!addressType) {
1214
+ addressType = this.data.addressType;
1215
+ }
1216
+ const network = this._network;
1217
+ const { path, type } = addressTypes[addressType];
1218
+ if (!keyDerivationPath) {
1219
+ const keyDerivationPathResponse = getKeyDerivationPathObject({
1220
+ path,
1221
+ network
1222
+ });
1223
+ if (keyDerivationPathResponse.isErr()) {
1224
+ return err(keyDerivationPathResponse.error.message);
1225
+ }
1226
+ keyDerivationPath = keyDerivationPathResponse.value;
1227
+ }
1228
+ const generatedAddresses = await this.generateAddresses({
1229
+ addressAmount,
1230
+ changeAddressAmount,
1231
+ addressIndex,
1232
+ changeAddressIndex,
1233
+ keyDerivationPath,
1234
+ addressType: type
1235
+ });
1236
+ if (generatedAddresses.isErr()) {
1237
+ return err(generatedAddresses.error);
1238
+ }
1239
+
1240
+ const removeDuplicateResponse = await this.removeDuplicateAddresses({
1241
+ addresses: generatedAddresses.value.addresses,
1242
+ changeAddresses: generatedAddresses.value.changeAddresses
1243
+ });
1244
+ if (removeDuplicateResponse.isErr()) {
1245
+ return err(removeDuplicateResponse.error.message);
1246
+ }
1247
+
1248
+ const addresses = removeDuplicateResponse.value.addresses;
1249
+ const changeAddresses = removeDuplicateResponse.value.changeAddresses;
1250
+ if (Object.keys(addresses).length) {
1251
+ this._data.addresses[addressType] = {
1252
+ ...this.data.addresses[addressType],
1253
+ ...addresses
1254
+ };
1255
+ await this.saveWalletData('addresses', this._data.addresses);
1256
+ }
1257
+ if (Object.keys(changeAddresses).length) {
1258
+ this._data.changeAddresses[addressType] = {
1259
+ ...this.data.changeAddresses[addressType],
1260
+ ...changeAddresses
1261
+ };
1262
+ await this.saveWalletData('changeAddresses', this._data.changeAddresses);
1263
+ }
1264
+
1265
+ return ok({ ...generatedAddresses.value, addressType: type });
1266
+ }
1267
+
1268
+ /**
1269
+ * This method will compare a set of specified addresses to the currently stored addresses and remove any duplicates.
1270
+ * @private
1271
+ * @async
1272
+ * @param {IAddresses} addresses
1273
+ * @param {IAddresses} changeAddresses
1274
+ * @returns {Promise<Result<IGenerateAddressesResponse>>}
1275
+ */
1276
+ private async removeDuplicateAddresses({
1277
+ addresses = {},
1278
+ changeAddresses = {}
1279
+ }: {
1280
+ addresses?: IAddresses;
1281
+ changeAddresses?: IAddresses;
1282
+ }): Promise<Result<IGenerateAddressesResponse>> {
1283
+ try {
1284
+ const currentWallet: IWalletData = this.data;
1285
+ const currentAddressTypeContent: TAddressTypeContent<IAddresses> =
1286
+ currentWallet.addresses;
1287
+ const currentChangeAddressTypeContent: TAddressTypeContent<IAddresses> =
1288
+ currentWallet.changeAddresses;
1289
+
1290
+ //Remove any duplicate addresses.
1291
+ await Promise.all([
1292
+ objectKeys(currentAddressTypeContent).map(async (addressType) => {
1293
+ await Promise.all(
1294
+ objectKeys(addresses).map((scriptHash) => {
1295
+ if (scriptHash in currentAddressTypeContent[addressType]) {
1296
+ delete addresses[scriptHash];
1297
+ }
1298
+ })
1299
+ );
1300
+ }),
1301
+
1302
+ objectKeys(currentChangeAddressTypeContent).map(async (addressType) => {
1303
+ await Promise.all(
1304
+ objectKeys(changeAddresses).map((scriptHash) => {
1305
+ if (scriptHash in currentChangeAddressTypeContent[addressType]) {
1306
+ delete changeAddresses[scriptHash];
1307
+ }
1308
+ })
1309
+ );
1310
+ })
1311
+ ]);
1312
+
1313
+ return ok({ addresses, changeAddresses });
1314
+ } catch (e) {
1315
+ return err(e);
1316
+ }
1317
+ }
1318
+
1319
+ /**
1320
+ * This method updates the next available (zero-balance) address & changeAddress index.
1321
+ * @private
1322
+ * @async
1323
+ * @param {EAddressType} [addressType]
1324
+ * @returns {string}
1325
+ */
1326
+ private async updateAddressIndexes({
1327
+ addressType //If this param is left undefined it will update the indexes for all stored address types.
1328
+ }: {
1329
+ addressType?: EAddressType;
1330
+ } = {}): Promise<Result<string>> {
1331
+ const checkRes = await this.checkElectrumConnection();
1332
+ if (checkRes.isErr()) return err(checkRes.error.message);
1333
+ const currentWallet = this.data;
1334
+
1335
+ let addressTypeKeys = Object.values(EAddressType);
1336
+ if (addressType) {
1337
+ addressTypeKeys = [addressType];
1338
+ }
1339
+
1340
+ let updated = false;
1341
+
1342
+ const promises = addressTypeKeys.map(async (addressTypeKey) => {
1343
+ const response = await this.getNextAvailableAddress(addressTypeKey);
1344
+ if (response.isErr()) {
1345
+ throw response.error;
1346
+ }
1347
+ const result = response.value;
1348
+ let addressIndex = currentWallet.addressIndex[addressTypeKey];
1349
+ let changeAddressIndex = currentWallet.changeAddressIndex[addressTypeKey];
1350
+ let lastUsedAddressIndex =
1351
+ currentWallet.lastUsedAddressIndex[addressTypeKey];
1352
+ let lastUsedChangeAddressIndex =
1353
+ currentWallet.lastUsedChangeAddressIndex[addressTypeKey];
1354
+ if (
1355
+ addressIndex.index < 0 ||
1356
+ changeAddressIndex.index < 0 ||
1357
+ result.addressIndex.index > addressIndex.index ||
1358
+ result.changeAddressIndex.index > changeAddressIndex.index ||
1359
+ result.lastUsedAddressIndex.index > lastUsedAddressIndex.index ||
1360
+ result.lastUsedChangeAddressIndex.index >
1361
+ lastUsedChangeAddressIndex?.index
1362
+ ) {
1363
+ if (result.addressIndex) {
1364
+ addressIndex = result.addressIndex;
1365
+ }
1366
+
1367
+ if (result.changeAddressIndex) {
1368
+ changeAddressIndex = result.changeAddressIndex;
1369
+ }
1370
+
1371
+ if (result.lastUsedAddressIndex) {
1372
+ lastUsedAddressIndex = result.lastUsedAddressIndex;
1373
+ }
1374
+
1375
+ if (result.lastUsedChangeAddressIndex) {
1376
+ lastUsedChangeAddressIndex = result.lastUsedChangeAddressIndex;
1377
+ }
1378
+
1379
+ //Final check to ensure that both addresses and change addresses do not exceed the gap limit/scanning threshold.
1380
+ //If either does, we generate a new addresses and/or change address at +1 the last used index.
1381
+ const lastUsedIndex =
1382
+ lastUsedAddressIndex.index > 0 ? lastUsedAddressIndex.index : 0;
1383
+ const currentGap = Math.abs(addressIndex.index - lastUsedIndex);
1384
+ if (currentGap > GAP_LIMIT) {
1385
+ const excessAmount = currentGap - GAP_LIMIT;
1386
+ const newIndex = addressIndex.index - excessAmount;
1387
+ const _addressIndex = await this.generateAddresses({
1388
+ addressType: addressTypeKey,
1389
+ addressAmount: 1,
1390
+ changeAddressAmount: 0,
1391
+ addressIndex: newIndex
1392
+ });
1393
+ if (_addressIndex.isErr()) {
1394
+ return err(_addressIndex.error.message);
1395
+ }
1396
+ addressIndex = Object.values(_addressIndex.value.addresses)[0];
1397
+ }
1398
+
1399
+ const lastUsedChangeIndex =
1400
+ lastUsedChangeAddressIndex.index > 0
1401
+ ? lastUsedChangeAddressIndex.index
1402
+ : 0;
1403
+ const currentChangeAddressGap = Math.abs(
1404
+ changeAddressIndex.index - lastUsedChangeIndex
1405
+ );
1406
+ if (currentChangeAddressGap > GAP_LIMIT) {
1407
+ const excessAmount = currentGap - GAP_LIMIT;
1408
+ const newIndex = addressIndex.index - excessAmount;
1409
+ const _changeAddressIndex = await this.generateAddresses({
1410
+ addressType: addressTypeKey,
1411
+ addressAmount: 0,
1412
+ changeAddressAmount: 1,
1413
+ changeAddressIndex: newIndex
1414
+ });
1415
+ if (_changeAddressIndex.isErr()) {
1416
+ return err(_changeAddressIndex.error.message);
1417
+ }
1418
+ changeAddressIndex = Object.values(
1419
+ _changeAddressIndex.value.changeAddresses
1420
+ )[0];
1421
+ }
1422
+
1423
+ this._data.addressIndex[addressTypeKey] = addressIndex;
1424
+ this._data.changeAddressIndex[addressTypeKey] = changeAddressIndex;
1425
+ this._data.lastUsedAddressIndex[addressTypeKey] = lastUsedAddressIndex;
1426
+ this._data.lastUsedChangeAddressIndex[addressTypeKey] =
1427
+ lastUsedChangeAddressIndex;
1428
+ await Promise.all([
1429
+ this.saveWalletData('addressIndex', this._data.addressIndex),
1430
+ this.saveWalletData(
1431
+ 'changeAddressIndex',
1432
+ this._data.changeAddressIndex
1433
+ ),
1434
+ this.saveWalletData(
1435
+ 'lastUsedAddressIndex',
1436
+ this._data.lastUsedAddressIndex
1437
+ ),
1438
+ this.saveWalletData(
1439
+ 'lastUsedChangeAddressIndex',
1440
+ this._data.lastUsedChangeAddressIndex
1441
+ )
1442
+ ]);
1443
+ updated = true;
1444
+ }
1445
+ });
1446
+ try {
1447
+ await Promise.all(promises);
1448
+ return ok(
1449
+ updated ? 'Successfully updated indexes.' : 'No update needed.'
1450
+ );
1451
+ } catch (e) {
1452
+ return err(e);
1453
+ }
1454
+ }
1455
+
1456
+ /**
1457
+ * Resets address indexes back to the app's default/original state.
1458
+ * @private
1459
+ * @returns {void}
1460
+ */
1461
+ public async resetAddressIndexes(): Promise<void> {
1462
+ const defaultWalletShape = getDefaultWalletData();
1463
+ this._data.addressIndex = defaultWalletShape.addressIndex;
1464
+ this._data.changeAddressIndex = defaultWalletShape.changeAddressIndex;
1465
+ this._data.lastUsedAddressIndex = defaultWalletShape.lastUsedAddressIndex;
1466
+ this._data.lastUsedChangeAddressIndex =
1467
+ defaultWalletShape.lastUsedChangeAddressIndex;
1468
+ await Promise.all([
1469
+ this.saveWalletData('addressIndex', this._data.addressIndex),
1470
+ this.saveWalletData('changeAddressIndex', this._data.changeAddressIndex),
1471
+ this.saveWalletData(
1472
+ 'lastUsedAddressIndex',
1473
+ this._data.lastUsedAddressIndex
1474
+ ),
1475
+ this.saveWalletData(
1476
+ 'lastUsedChangeAddressIndex',
1477
+ this._data.lastUsedChangeAddressIndex
1478
+ )
1479
+ ]);
1480
+ }
1481
+
1482
+ /**
1483
+ * Generate a new receive address for the provided addresstype up to the set gap limit.
1484
+ * @async
1485
+ * @param {EAddressType} addressType
1486
+ * @param {IKeyDerivationPath} keyDerivationPath
1487
+ * @returns {Promise<Result<IAddress>>}
1488
+ */
1489
+ public async generateNewReceiveAddress({
1490
+ addressType = this.data.addressType,
1491
+ keyDerivationPath
1492
+ }: {
1493
+ addressType?: EAddressType;
1494
+ keyDerivationPath?: IKeyDerivationPath;
1495
+ }): Promise<Result<IAddress>> {
1496
+ try {
1497
+ const network = this._network;
1498
+ const currentWallet = this.data;
1499
+
1500
+ const getGapLimitResponse = this.getGapLimit({
1501
+ addressType
1502
+ });
1503
+ if (getGapLimitResponse.isErr()) {
1504
+ return err(getGapLimitResponse.error.message);
1505
+ }
1506
+ const { addressDelta } = getGapLimitResponse.value;
1507
+
1508
+ // If the address delta exceeds the default gap limit, only return the current address index.
1509
+ if (addressDelta >= GAP_LIMIT) {
1510
+ const addressIndex = currentWallet.addressIndex;
1511
+ const receiveAddress = addressIndex[addressType];
1512
+ return ok(receiveAddress);
1513
+ }
1514
+
1515
+ const { path } = addressTypes[addressType];
1516
+ if (!keyDerivationPath) {
1517
+ const keyDerivationPathResponse = getKeyDerivationPathObject({
1518
+ network,
1519
+ path
1520
+ });
1521
+ if (keyDerivationPathResponse.isErr()) {
1522
+ return err(keyDerivationPathResponse.error.message);
1523
+ }
1524
+ keyDerivationPath = keyDerivationPathResponse.value;
1525
+ }
1526
+ const addresses: IAddresses = currentWallet.addresses[addressType];
1527
+ const currentAddressIndex: number =
1528
+ currentWallet.addressIndex[addressType].index;
1529
+ const nextAddressIndex = Object.values(addresses).find((address) => {
1530
+ return address.index === currentAddressIndex + 1;
1531
+ });
1532
+
1533
+ // Check if the next address index already exists or if it needs to be generated.
1534
+ if (nextAddressIndex) {
1535
+ // Update addressIndex and return the address content.
1536
+ this._data.addressIndex[addressType] = nextAddressIndex;
1537
+ await Promise.all([
1538
+ this.saveWalletData('addressIndex', this._data.addressIndex)
1539
+ ]);
1540
+ return ok(nextAddressIndex);
1541
+ }
1542
+
1543
+ // We need to generate, save and return the new address.
1544
+ const addAddressesRes = await this.addAddresses({
1545
+ addressAmount: 1,
1546
+ changeAddressAmount: 0,
1547
+ addressIndex: currentAddressIndex + 1,
1548
+ changeAddressIndex: 0,
1549
+ keyDerivationPath,
1550
+ addressType
1551
+ });
1552
+ if (addAddressesRes.isErr()) {
1553
+ return err(addAddressesRes.error.message);
1554
+ }
1555
+ const addressKeys = Object.keys(addAddressesRes.value.addresses);
1556
+ // If for any reason the phone was unable to generate the new address, return error.
1557
+ if (!addressKeys.length) {
1558
+ return err('Unable to generate addresses at this time.');
1559
+ }
1560
+ const newAddressIndex: IAddress =
1561
+ addAddressesRes.value.addresses[addressKeys[0]];
1562
+ this._data.addressIndex[addressType] = newAddressIndex;
1563
+
1564
+ await this.saveWalletData('addressIndex', this._data.addressIndex);
1565
+ return ok(newAddressIndex);
1566
+ } catch (e) {
1567
+ return err(e);
1568
+ }
1569
+ }
1570
+
1571
+ /**
1572
+ * Returns the difference between the current address index and the last used address index.
1573
+ * @private
1574
+ * @param {EAddressType} [addressType]
1575
+ * @returns {Result<{ addressDelta: number; changeAddressDelta: number }>}
1576
+ */
1577
+ public getGapLimit({
1578
+ addressType
1579
+ }: {
1580
+ addressType?: EAddressType;
1581
+ }): Result<{ addressDelta: number; changeAddressDelta: number }> {
1582
+ try {
1583
+ if (!addressType) {
1584
+ addressType = this.data.addressType;
1585
+ }
1586
+ const currentWallet = this.data;
1587
+ const addressIndex = currentWallet.addressIndex[addressType].index;
1588
+ const lastUsedAddressIndex =
1589
+ currentWallet.lastUsedAddressIndex[addressType].index;
1590
+ const changeAddressIndex =
1591
+ currentWallet.changeAddressIndex[addressType].index;
1592
+ const lastUsedChangeAddressIndex =
1593
+ currentWallet.lastUsedChangeAddressIndex[addressType].index;
1594
+ const addressDelta = Math.abs(
1595
+ addressIndex - (lastUsedAddressIndex > 0 ? lastUsedAddressIndex : 0)
1596
+ );
1597
+ const changeAddressDelta = Math.abs(
1598
+ changeAddressIndex -
1599
+ (lastUsedChangeAddressIndex > 0 ? lastUsedChangeAddressIndex : 0)
1600
+ );
1601
+
1602
+ return ok({ addressDelta, changeAddressDelta });
1603
+ } catch (e) {
1604
+ return err(e);
1605
+ }
1606
+ }
1607
+
1608
+ /**
1609
+ * Retrieves and sets UTXO's for the current wallet from Electrum.
1610
+ * @param {boolean} scanAllAddresses
1611
+ * @returns {Promise<Result<IGetUtxosResponse>>}
1612
+ */
1613
+ public async getUtxos({
1614
+ scanAllAddresses = false
1615
+ }): Promise<Result<IGetUtxosResponse>> {
1616
+ const checkRes = await this.checkElectrumConnection();
1617
+ if (checkRes.isErr()) return err(checkRes.error.message);
1618
+ const getUtxosRes = await this.electrum.getUtxos({ scanAllAddresses });
1619
+ if (getUtxosRes.isErr()) {
1620
+ return err(getUtxosRes.error.message);
1621
+ }
1622
+ const utxos = getUtxosRes.value?.utxos ?? [];
1623
+ const balance = getUtxosRes.value?.balance ?? 0;
1624
+ this._data.utxos = utxos;
1625
+ this._data.balance = balance;
1626
+ await Promise.all([
1627
+ this.saveWalletData('utxos', this._data.utxos),
1628
+ this.saveWalletData('balance', this._data.balance)
1629
+ ]);
1630
+ return ok({ utxos, balance });
1631
+ }
1632
+
1633
+ /**
1634
+ * Returns the current wallet's UTXO's from storage.
1635
+ * @returns {IUtxo[]}
1636
+ */
1637
+ listUtxos(): IUtxo[] {
1638
+ return this.data.utxos;
1639
+ }
1640
+
1641
+ /**
1642
+ * Saves the wallet data object to storage if able.
1643
+ * @private
1644
+ * @async
1645
+ * @param {TWalletDataKeys} key
1646
+ * @param {IWalletData[K]} data
1647
+ * @returns {Promise<void>}
1648
+ */
1649
+ public async saveWalletData<K extends keyof IWalletData>(
1650
+ key: TWalletDataKeys,
1651
+ data: IWalletData[K]
1652
+ ): Promise<void> {
1653
+ if (!this._setData) return;
1654
+ const walletDataKey = this.getWalletDataKey(key);
1655
+ await this._setData(walletDataKey, data);
1656
+ }
1657
+
1658
+ //TODO: Implement this as a way to better update and save state so we can consolidate this.data[key] updates.
1659
+ // @ts-ignore
1660
+ private async updateAndSaveWalletData(
1661
+ key: TWalletDataKeys,
1662
+ data: IWalletData,
1663
+ addressType?: EAddressType
1664
+ ): Promise<void> {
1665
+ if (addressType) {
1666
+ this.data[key][addressType] = data;
1667
+ await this.saveWalletData(key, this._data[key]);
1668
+ } else {
1669
+ // @ts-ignore
1670
+ this.data[key] = data;
1671
+ await this.saveWalletData(key, this._data[key]);
1672
+ }
1673
+ }
1674
+
1675
+ /**
1676
+ * Retrieves, formats & stores the transaction history for the selected wallet/network.
1677
+ * @param {boolean} [scanAllAddresses]
1678
+ * @param {boolean} [replaceStoredTransactions] Setting this to true will set scanAllAddresses to true as well.
1679
+ * @returns {Promise<Result<string | undefined>>}
1680
+ */
1681
+ public async updateTransactions({
1682
+ scanAllAddresses = false,
1683
+ replaceStoredTransactions = false
1684
+ }: {
1685
+ scanAllAddresses?: boolean;
1686
+ replaceStoredTransactions?: boolean;
1687
+ }): Promise<Result<string | undefined>> {
1688
+ //Check existing unconfirmed transactions and remove any that are confirmed.
1689
+ //If the tx is reorg'd or bumped from the mempool and no longer exists, the transaction will be removed from the store and updated in the activity list.
1690
+ await this.checkUnconfirmedTransactions();
1691
+
1692
+ const history = await this.electrum.getAddressHistory({
1693
+ scanAllAddresses: scanAllAddresses || replaceStoredTransactions
1694
+ });
1695
+ if (history.isErr()) {
1696
+ return err(history.error.message);
1697
+ }
1698
+ if (!history.value.length) {
1699
+ return ok(undefined);
1700
+ }
1701
+
1702
+ const getTransactionsResponse = await this.electrum.getTransactions({
1703
+ txHashes: history.value
1704
+ });
1705
+ if (getTransactionsResponse.isErr()) {
1706
+ return err(getTransactionsResponse.error.message);
1707
+ }
1708
+
1709
+ const formatTransactionsResponse = await this.formatTransactions({
1710
+ transactions: getTransactionsResponse.value.data
1711
+ });
1712
+ if (formatTransactionsResponse.isErr()) {
1713
+ return err(formatTransactionsResponse.error.message);
1714
+ }
1715
+ const transactions = formatTransactionsResponse.value;
1716
+
1717
+ // Add unconfirmed transactions.
1718
+ // No need to wait for this to finish.
1719
+ this.addUnconfirmedTransactions({
1720
+ transactions
1721
+ });
1722
+
1723
+ if (replaceStoredTransactions) {
1724
+ // No need to check the existing txs since we're replacing them. Update with the returned formatTransactionsResponse.
1725
+ this._data.transactions = transactions;
1726
+ await this.saveWalletData('transactions', this._data.transactions);
1727
+ return ok(undefined);
1728
+ }
1729
+
1730
+ // Handle new or updated transactions.
1731
+ const formattedTransactions: IFormattedTransactions = {};
1732
+
1733
+ let notificationTxid: string | undefined;
1734
+ const storedTransactions = this.data.transactions;
1735
+ const confirmedTxs: TTransactionMessage[] = [];
1736
+ const receivedTxs: TTransactionMessage[] = [];
1737
+ const sentTxs: TTransactionMessage[] = [];
1738
+
1739
+ Object.keys(transactions).forEach((txid) => {
1740
+ //If the tx is new or the tx now has a block height (state changed to confirmed)
1741
+ if (
1742
+ !storedTransactions[txid] ||
1743
+ storedTransactions[txid].height !== transactions[txid].height
1744
+ ) {
1745
+ formattedTransactions[txid] = {
1746
+ ...transactions[txid],
1747
+ // Keep the previous timestamp if the tx is not new.
1748
+ timestamp:
1749
+ storedTransactions[txid]?.timestamp ??
1750
+ transactions[txid]?.timestamp ??
1751
+ Date.now()
1752
+ };
1753
+ if (this.onMessage && formattedTransactions[txid]?.height > 0)
1754
+ confirmedTxs.push({ transaction: formattedTransactions[txid] });
1755
+ }
1756
+
1757
+ // if the tx is new, incoming but not from a transfer - show notification
1758
+ if (!(txid in storedTransactions)) {
1759
+ if (this.onMessage) {
1760
+ if (transactions[txid].type === EPaymentType.received) {
1761
+ receivedTxs.push({ transaction: transactions[txid] });
1762
+ } else if (transactions[txid].type === EPaymentType.sent) {
1763
+ sentTxs.push({ transaction: transactions[txid] });
1764
+ }
1765
+ }
1766
+ notificationTxid = txid;
1767
+ }
1768
+ });
1769
+
1770
+ //No new or updated transactions
1771
+ if (!Object.keys(formattedTransactions).length) {
1772
+ return ok(undefined);
1773
+ }
1774
+
1775
+ this._data.transactions = {
1776
+ ...this._data.transactions,
1777
+ ...formattedTransactions
1778
+ };
1779
+ await this.saveWalletData('transactions', this._data.transactions);
1780
+
1781
+ if (this.onMessage) {
1782
+ confirmedTxs.forEach((tx) => {
1783
+ // @ts-ignore
1784
+ this.onMessage('transactionConfirmed', tx);
1785
+ });
1786
+ sentTxs.forEach((tx) => {
1787
+ // @ts-ignore
1788
+ this.onMessage('transactionSent', tx);
1789
+ });
1790
+ receivedTxs.forEach((tx) => {
1791
+ // @ts-ignore
1792
+ this.onMessage('transactionReceived', tx);
1793
+ });
1794
+ }
1795
+
1796
+ return ok(notificationTxid);
1797
+ }
1798
+
1799
+ /**
1800
+ * Checks existing unconfirmed transactions that have been received and removes any that have >= 6 confirmations.
1801
+ * If the tx is reorg'd or bumped from the mempool and no longer exists, the transaction
1802
+ * will be removed from the store and updated in the activity list.
1803
+ * @private
1804
+ * @async
1805
+ * @returns {Promise<Result<string>>}
1806
+ */
1807
+ private async checkUnconfirmedTransactions(): Promise<Result<string>> {
1808
+ try {
1809
+ const processRes = await this.processUnconfirmedTransactions();
1810
+ if (processRes.isErr()) {
1811
+ return err(processRes.error.message);
1812
+ }
1813
+
1814
+ const { unconfirmedTxs, outdatedTxs, ghostTxs } = processRes.value;
1815
+ if (outdatedTxs.length > 0) {
1816
+ if (this.onMessage) this.onMessage('reorg', outdatedTxs);
1817
+ //We need to update the height of the transactions that were reorg'd out.
1818
+ await this.updateTransactionHeights(outdatedTxs);
1819
+ }
1820
+ if (ghostTxs.length > 0) {
1821
+ if (this.onMessage) this.onMessage('rbf', ghostTxs);
1822
+ //We need to update the ghost transactions in the store & activity-list and rescan the addresses to get the correct balance.
1823
+ await this.updateGhostTransactions({
1824
+ txIds: ghostTxs
1825
+ });
1826
+ } else {
1827
+ this._data.unconfirmedTransactions = unconfirmedTxs;
1828
+ await Promise.all([
1829
+ this.saveWalletData(
1830
+ 'unconfirmedTransactions',
1831
+ this._data.unconfirmedTransactions
1832
+ )
1833
+ ]);
1834
+ }
1835
+ return ok('Successfully updated unconfirmed transactions.');
1836
+ } catch (e) {
1837
+ return err(e);
1838
+ }
1839
+ }
1840
+
1841
+ /**
1842
+ * This method processes all transactions with less than 6 confirmations and returns the following:
1843
+ * 1. Transactions that still have less than 6 confirmations and can be considered unconfirmed. (unconfirmedTxs)
1844
+ * 2. Transactions that have fewer confirmations than before due to a reorg. (outdatedTxs)
1845
+ * 3. Transactions that have been removed from the mempool. (ghostTxs)
1846
+ * @private
1847
+ * @async
1848
+ * @returns {Promise<Result<TProcessUnconfirmedTransactions>>}
1849
+ */
1850
+ private async processUnconfirmedTransactions(): Promise<
1851
+ Result<TProcessUnconfirmedTransactions>
1852
+ > {
1853
+ try {
1854
+ //Retrieve all unconfirmed transactions (tx less than 6 confirmations in this case) from the store
1855
+ const oldUnconfirmedTxs = this.getUnconfirmedTransactions();
1856
+
1857
+ //Use electrum to check if the transaction was removed/bumped from the mempool or if it still exists.
1858
+ const tx_hashes: ITxHash[] = Object.values(oldUnconfirmedTxs).map(
1859
+ (transaction: IFormattedTransaction) => {
1860
+ return { tx_hash: transaction.txid };
1861
+ }
1862
+ );
1863
+ const txs = await this.electrum.getTransactions({
1864
+ txHashes: tx_hashes
1865
+ });
1866
+ if (txs.isErr()) {
1867
+ return err(txs.error);
1868
+ }
1869
+
1870
+ const unconfirmedTxs: IFormattedTransactions = {};
1871
+ const outdatedTxs: IUtxo[] = []; //Transactions that have been pushed back into the mempool due to a reorg. We need to update the height.
1872
+ const ghostTxs: string[] = []; //Transactions that have been removed from the mempool and are no longer in the blockchain.
1873
+ txs.value.data.forEach((txData: ITransaction<IUtxo>) => {
1874
+ // Check if the transaction has been removed from the mempool/still exists.
1875
+ if (!this.electrum.transactionExists(txData)) {
1876
+ //Transaction may have been removed/bumped from the mempool or potentially reorg'd out.
1877
+ ghostTxs.push(txData.data.tx_hash);
1878
+ return;
1879
+ }
1880
+
1881
+ const newHeight = this.confirmationsToBlockHeight({
1882
+ confirmations: txData.result?.confirmations ?? 0
1883
+ });
1884
+
1885
+ if (!txData.result?.confirmations) {
1886
+ const oldHeight = oldUnconfirmedTxs[txData.data.tx_hash]?.height ?? 0;
1887
+ if (oldHeight > newHeight) {
1888
+ //Transaction was reorg'd back to zero confirmations. Add it to the outdatedTxs array.
1889
+ outdatedTxs.push(txData.data);
1890
+ }
1891
+ unconfirmedTxs[txData.data.tx_hash] = {
1892
+ ...oldUnconfirmedTxs[txData.data.tx_hash],
1893
+ height: newHeight
1894
+ };
1895
+ return;
1896
+ }
1897
+
1898
+ //Check if the transaction has been confirmed.
1899
+ if (txData.result?.confirmations < 6) {
1900
+ unconfirmedTxs[txData.data.tx_hash] = {
1901
+ ...oldUnconfirmedTxs[txData.data.tx_hash],
1902
+ height: newHeight
1903
+ };
1904
+ }
1905
+ });
1906
+ return ok({
1907
+ unconfirmedTxs,
1908
+ outdatedTxs,
1909
+ ghostTxs
1910
+ });
1911
+ } catch (e) {
1912
+ return err(e);
1913
+ }
1914
+ }
1915
+
1916
+ /**
1917
+ * Returns the current wallet's unconfirmed transactions from storage.
1918
+ * @returns {Promise<Result<IFormattedTransactions>>}
1919
+ */
1920
+ public getUnconfirmedTransactions(): IFormattedTransactions {
1921
+ const currentWallet = this.data;
1922
+ return currentWallet?.unconfirmedTransactions ?? {};
1923
+ }
1924
+
1925
+ /**
1926
+ * Returns the block height for a given number of confirmations from storage.
1927
+ * @param {number} confirmations
1928
+ * @param {number} [currentHeight]
1929
+ * @param {TAvailableNetworks} [selectedNetwork]
1930
+ * @returns {number}
1931
+ */
1932
+ public confirmationsToBlockHeight({
1933
+ confirmations,
1934
+ currentHeight
1935
+ }: {
1936
+ confirmations: number;
1937
+ currentHeight?: number;
1938
+ }): number {
1939
+ if (!currentHeight) {
1940
+ const header = this.data.header;
1941
+ currentHeight = header.height;
1942
+ }
1943
+ if (confirmations > currentHeight) {
1944
+ return 0;
1945
+ }
1946
+ return currentHeight - confirmations;
1947
+ }
1948
+
1949
+ /**
1950
+ * Updates & Saves header information to storage.
1951
+ * @param headerData
1952
+ * @returns {Promise<void>}
1953
+ */
1954
+ public async updateHeader(headerData: IHeader): Promise<void> {
1955
+ this._data.header = headerData;
1956
+ await this.saveWalletData('header', headerData);
1957
+ }
1958
+
1959
+ /**
1960
+ * Removes transactions from the store and activity list.
1961
+ * @private
1962
+ * @async
1963
+ * @param {string[]} txIds
1964
+ * @returns {Promise<Result<string>>}
1965
+ */
1966
+ private async updateGhostTransactions({
1967
+ txIds
1968
+ }: {
1969
+ txIds: string[];
1970
+ }): Promise<Result<string>> {
1971
+ try {
1972
+ const transactions = this.data.transactions;
1973
+ const unconfirmedTransactions = this.data.unconfirmedTransactions;
1974
+ txIds.forEach((txId) => {
1975
+ if (txId in transactions) {
1976
+ transactions[txId]['exists'] = false;
1977
+ }
1978
+ if (txId in unconfirmedTransactions) {
1979
+ delete unconfirmedTransactions[txId];
1980
+ }
1981
+ });
1982
+ this._data.transactions = transactions;
1983
+ await this.saveWalletData('transactions', transactions);
1984
+ this._data.unconfirmedTransactions = unconfirmedTransactions;
1985
+ await this.saveWalletData(
1986
+ 'unconfirmedTransactions',
1987
+ unconfirmedTransactions
1988
+ );
1989
+
1990
+ //Rescan the addresses to get the correct balance.
1991
+ await this.rescanAddresses({
1992
+ shouldClearAddresses: false // No need to clear addresses since we are only updating the balance.
1993
+ });
1994
+ return ok('Successfully deleted transactions.');
1995
+ } catch (e) {
1996
+ return err(e);
1997
+ }
1998
+ }
1999
+
2000
+ /**
2001
+ * This method will clear the utxo array for each address type and reset the
2002
+ * address indexes back to the original/default app values. Once cleared & reset
2003
+ * the app will rescan the wallet's addresses from index zero.
2004
+ * @async
2005
+ * @param {boolean} [shouldClearAddresses] - Clears and re-generates all addresses when true.
2006
+ * @param shouldClearTransactions
2007
+ * @returns {Promise<Result<string>>}
2008
+ */
2009
+ public async rescanAddresses({
2010
+ shouldClearAddresses = true, // It's assumed we want to clear addresses in this method unless explicitly set to false.
2011
+ shouldClearTransactions = false // We'll lose some timestamp information about the transactions if we clear them. So it's set to false by default.
2012
+ }: {
2013
+ shouldClearAddresses?: boolean;
2014
+ shouldClearTransactions?: boolean;
2015
+ }): Promise<Result<IWalletData>> {
2016
+ if (shouldClearAddresses) {
2017
+ await this.clearAddresses();
2018
+ }
2019
+ if (shouldClearTransactions) {
2020
+ await this.clearTransactions();
2021
+ }
2022
+ await this.clearUtxos();
2023
+ await this.resetAddressIndexes();
2024
+ // Wait to generate our zero index addresses.
2025
+ await this.setZeroIndexAddresses();
2026
+ return await this.refreshWallet({
2027
+ scanAllAddresses: true
2028
+ });
2029
+ }
2030
+
2031
+ /**
2032
+ * Clears the UTXO array and balance from storage.
2033
+ * @private
2034
+ * @async
2035
+ * @returns {Promise<string>}
2036
+ */
2037
+ private async clearUtxos(): Promise<string> {
2038
+ this._data.balance = 0;
2039
+ this._data.utxos = [];
2040
+ await Promise.all([
2041
+ this.saveWalletData('balance', this._data.balance),
2042
+ this.saveWalletData('utxos', this._data.utxos)
2043
+ ]);
2044
+ return "Successfully cleared UTXO's.";
2045
+ }
2046
+
2047
+ /**
2048
+ * Clears the transactions object for a given wallet and network from storage.
2049
+ * @private
2050
+ * @returns {string}
2051
+ */
2052
+ private async clearTransactions(): Promise<string> {
2053
+ this._data.transactions = getDefaultWalletData().transactions;
2054
+ await this.saveWalletData('transactions', this._data.transactions);
2055
+ return 'Successfully reset transactions.';
2056
+ }
2057
+
2058
+ /**
2059
+ * Clears the addresses and changeAddresses object for a given wallet and network.
2060
+ * @private
2061
+ * @async
2062
+ * @returns {Promise<string>}
2063
+ */
2064
+ private async clearAddresses(): Promise<string> {
2065
+ this._data.addresses = getAddressTypeContent<IAddresses>({});
2066
+ this._data.changeAddresses = getAddressTypeContent<IAddresses>({});
2067
+ await Promise.all([
2068
+ this.saveWalletData('addresses', this._data.addresses),
2069
+ this.saveWalletData('changeAddresses', this._data.changeAddresses)
2070
+ ]);
2071
+ return 'Successfully reset transactions.';
2072
+ }
2073
+
2074
+ /**
2075
+ * Updates the confirmation state of activity item transactions that were reorg'd out.
2076
+ * @private
2077
+ * @async
2078
+ * @param {IUtxo[]} txs
2079
+ * @returns {Promise<string>}
2080
+ */
2081
+ private async updateTransactionHeights(txs: IUtxo[]): Promise<string> {
2082
+ let needsSave = false;
2083
+ const transactions = this.data.transactions;
2084
+ txs.forEach((tx) => {
2085
+ const txId = tx.tx_hash;
2086
+ if (txId in transactions) {
2087
+ transactions[txId].confirmTimestamp = 0;
2088
+ needsSave = true;
2089
+ }
2090
+ });
2091
+ if (needsSave) {
2092
+ await this.saveWalletData('transactions', transactions);
2093
+ }
2094
+ return 'Successfully updated reorg transactions.';
2095
+ }
2096
+
2097
+ /**
2098
+ * Parses and adds unconfirmed transactions to the store.
2099
+ * @private
2100
+ * @async
2101
+ * @param {IFormattedTransactions} transactions
2102
+ * @returns {Result<string>}
2103
+ */
2104
+ private async addUnconfirmedTransactions({
2105
+ transactions
2106
+ }: {
2107
+ transactions: IFormattedTransactions;
2108
+ }): Promise<Result<string>> {
2109
+ try {
2110
+ const unconfirmedTransactions: IFormattedTransactions = {};
2111
+ Object.keys(transactions).forEach((key) => {
2112
+ const confirmations = this.blockHeightToConfirmations({
2113
+ blockHeight: transactions[key].height
2114
+ });
2115
+ if (confirmations < 6) {
2116
+ unconfirmedTransactions[key] = transactions[key];
2117
+ }
2118
+ });
2119
+
2120
+ if (!Object.keys(unconfirmedTransactions).length) {
2121
+ return ok('No unconfirmed transactions found.');
2122
+ }
2123
+
2124
+ this._data.unconfirmedTransactions = {
2125
+ ...this.data.unconfirmedTransactions,
2126
+ ...unconfirmedTransactions
2127
+ };
2128
+
2129
+ await this.saveWalletData(
2130
+ 'unconfirmedTransactions',
2131
+ this._data.unconfirmedTransactions
2132
+ );
2133
+ return ok('Successfully updated unconfirmed transactions.');
2134
+ } catch (e) {
2135
+ console.log(e);
2136
+ return err(e);
2137
+ }
2138
+ }
2139
+
2140
+ /**
2141
+ * Returns the number of confirmations for a given block height.
2142
+ * @param {number} height
2143
+ * @param {number} [currentHeight]
2144
+ * @returns {number}
2145
+ */
2146
+ public blockHeightToConfirmations({
2147
+ blockHeight,
2148
+ currentHeight
2149
+ }: {
2150
+ blockHeight?: number;
2151
+ currentHeight?: number;
2152
+ }): number {
2153
+ if (!blockHeight) {
2154
+ return 0;
2155
+ }
2156
+ if (!currentHeight) {
2157
+ const header = this.electrum.getBlockHeader();
2158
+ currentHeight = header.height;
2159
+ }
2160
+ if (currentHeight < blockHeight) {
2161
+ return 0;
2162
+ }
2163
+ return currentHeight - blockHeight + 1;
2164
+ }
2165
+
2166
+ /**
2167
+ * Formats the provided transaction.
2168
+ * @async
2169
+ * @param {ITransaction<IUtxo>[]} transactions
2170
+ * @returns {Promise<Result<IFormattedTransactions>>}
2171
+ */
2172
+ public async formatTransactions({
2173
+ transactions
2174
+ }: {
2175
+ transactions: ITransaction<IUtxo>[];
2176
+ }): Promise<Result<IFormattedTransactions>> {
2177
+ if (transactions.length < 1) {
2178
+ return ok({});
2179
+ }
2180
+ const currentWallet = this.data;
2181
+
2182
+ // Batch and pre-fetch input data.
2183
+ const inputs: { tx_hash: string; vout: number }[] = [];
2184
+ transactions.forEach(({ result }) => {
2185
+ if (result?.vin) {
2186
+ result.vin.forEach((v) =>
2187
+ inputs.push({ tx_hash: v.txid, vout: v.vout })
2188
+ );
2189
+ }
2190
+ });
2191
+ const inputDataResponse = await this.getInputData({
2192
+ inputs
2193
+ });
2194
+ if (inputDataResponse.isErr()) {
2195
+ return err(inputDataResponse.error.message);
2196
+ }
2197
+ const addressTypeKeys = Object.values(EAddressType);
2198
+ const inputData = inputDataResponse.value;
2199
+ const currentAddresses = currentWallet.addresses;
2200
+ const currentChangeAddresses = currentWallet.changeAddresses;
2201
+
2202
+ let addresses = {} as IAddresses;
2203
+ let changeAddresses = {} as IAddresses;
2204
+ let rbf = false;
2205
+
2206
+ addressTypeKeys.map((addressType) => {
2207
+ // Check if addresses of this type have been generated. If not, skip.
2208
+ if (Object.keys(currentAddresses[addressType])?.length > 0) {
2209
+ addresses = {
2210
+ ...addresses,
2211
+ ...currentAddresses[addressType]
2212
+ };
2213
+ }
2214
+ // Check if change addresses of this type have been generated. If not, skip.
2215
+ if (Object.keys(currentChangeAddresses[addressType])?.length > 0) {
2216
+ changeAddresses = {
2217
+ ...changeAddresses,
2218
+ ...currentChangeAddresses[addressType]
2219
+ };
2220
+ }
2221
+ });
2222
+
2223
+ // Create combined address/change-address object for easier/faster reference later on.
2224
+ const combinedAddressObj: { [key: string]: IAddress } = {};
2225
+ [...Object.values(addresses), ...Object.values(changeAddresses)].map(
2226
+ (data) => {
2227
+ combinedAddressObj[data.address] = data;
2228
+ }
2229
+ );
2230
+
2231
+ const formattedTransactions: IFormattedTransactions = {};
2232
+ transactions.map(async ({ data, result }) => {
2233
+ if (!result.txid) {
2234
+ return;
2235
+ }
2236
+
2237
+ let totalInputValue = 0; // Total value of all inputs.
2238
+ let matchedInputValue = 0; // Total value of all inputs with addresses that belong to this wallet.
2239
+ let totalOutputValue = 0; // Total value of all outputs.
2240
+ let matchedOutputValue = 0; // Total value of all outputs with addresses that belong to this wallet.
2241
+ let messages: string[] = []; // Array of OP_RETURN messages.
2242
+
2243
+ //Iterate over each input
2244
+ result.vin.map(({ txid, scriptSig, vout, sequence }) => {
2245
+ //Push any OP_RETURN messages to messages array
2246
+ try {
2247
+ const asm = scriptSig.asm;
2248
+ if (asm !== '' && asm.includes('OP_RETURN')) {
2249
+ const OpReturnMessages = decodeOpReturnMessage(asm);
2250
+ messages = messages.concat(OpReturnMessages);
2251
+ }
2252
+ } catch {}
2253
+
2254
+ try {
2255
+ // Check if rbf was enabled for this transaction.
2256
+ if (sequence < 0xffffffff - 1) rbf = true;
2257
+ } catch {}
2258
+
2259
+ const { addresses: _addresses, value } = inputData[`${txid}${vout}`];
2260
+ totalInputValue = totalInputValue + value;
2261
+ _addresses.map((address) => {
2262
+ if (address in combinedAddressObj) {
2263
+ matchedInputValue = matchedInputValue + value;
2264
+ }
2265
+ });
2266
+ });
2267
+
2268
+ //Iterate over each output
2269
+ result.vout.map(({ scriptPubKey, value }) => {
2270
+ const _addresses = scriptPubKey.addresses
2271
+ ? scriptPubKey.addresses
2272
+ : scriptPubKey.address
2273
+ ? [scriptPubKey.address]
2274
+ : [];
2275
+ totalOutputValue = totalOutputValue + value;
2276
+ _addresses.map((address) => {
2277
+ if (address in combinedAddressObj) {
2278
+ matchedOutputValue = matchedOutputValue + value;
2279
+ }
2280
+ });
2281
+ });
2282
+
2283
+ const txid = result.txid;
2284
+ const type =
2285
+ matchedInputValue > matchedOutputValue
2286
+ ? EPaymentType.sent
2287
+ : EPaymentType.received;
2288
+ const totalMatchedValue = matchedOutputValue - matchedInputValue;
2289
+ const value = Number(totalMatchedValue.toFixed(8));
2290
+ const totalValue = totalInputValue - totalOutputValue;
2291
+ const fee = Number(Math.abs(totalValue).toFixed(8));
2292
+ const vsize = result.vsize;
2293
+ const satsPerByte = Math.round(btcToSats(fee) / vsize);
2294
+ const { address, height, scriptHash } = data;
2295
+ let timestamp = Date.now();
2296
+ let confirmTimestamp: number | undefined;
2297
+
2298
+ if (height > 0 && result.blocktime) {
2299
+ confirmTimestamp = result.blocktime * 1000;
2300
+ //In the event we're recovering, set the older timestamp.
2301
+ if (confirmTimestamp < timestamp) {
2302
+ timestamp = confirmTimestamp;
2303
+ }
2304
+ }
2305
+
2306
+ formattedTransactions[txid] = {
2307
+ address,
2308
+ height,
2309
+ scriptHash,
2310
+ totalInputValue,
2311
+ matchedInputValue,
2312
+ totalOutputValue,
2313
+ matchedOutputValue,
2314
+ fee,
2315
+ satsPerByte,
2316
+ type,
2317
+ value,
2318
+ txid,
2319
+ messages,
2320
+ timestamp,
2321
+ confirmTimestamp,
2322
+ vin: result.vin,
2323
+ rbf,
2324
+ exists: true,
2325
+ vsize
2326
+ };
2327
+ });
2328
+
2329
+ return ok(formattedTransactions);
2330
+ }
2331
+
2332
+ /**
2333
+ * Returns formatted input data from the inputs array.
2334
+ * @async
2335
+ * @param {{tx_hash: string, vout: number}[]} inputs
2336
+ * @returns {Promise<Result<InputData>>}
2337
+ */
2338
+ public async getInputData({
2339
+ inputs
2340
+ }: {
2341
+ inputs: { tx_hash: string; vout: number }[];
2342
+ }): Promise<Result<InputData>> {
2343
+ try {
2344
+ const inputData: InputData = {};
2345
+
2346
+ for (let i = 0; i < inputs.length; i += CHUNK_LIMIT) {
2347
+ const chunk = inputs.slice(i, i + CHUNK_LIMIT);
2348
+
2349
+ const getTransactionsResponse =
2350
+ await this.electrum.getTransactionsFromInputs({
2351
+ txHashes: chunk
2352
+ });
2353
+ if (getTransactionsResponse.isErr()) {
2354
+ return err(getTransactionsResponse.error.message);
2355
+ }
2356
+ getTransactionsResponse.value.data.map(({ data, result }) => {
2357
+ const vout = result.vout[data.vout];
2358
+ const addresses = vout.scriptPubKey.addresses
2359
+ ? vout.scriptPubKey.addresses
2360
+ : vout.scriptPubKey.address
2361
+ ? [vout.scriptPubKey.address]
2362
+ : [];
2363
+ const value = vout.value;
2364
+ const key = `${data.tx_hash}${vout.n}`;
2365
+ inputData[key] = { addresses, value };
2366
+ });
2367
+ }
2368
+ return ok(inputData);
2369
+ } catch (e) {
2370
+ return err(e);
2371
+ }
2372
+ }
2373
+
2374
+ /**
2375
+ * Returns exchange rate data from storage.
2376
+ * @param {string} currency
2377
+ * @returns {number}
2378
+ */
2379
+ public getExchangeRate(currency = 'EUR'): number {
2380
+ try {
2381
+ if (currency in this.exchangeRates) {
2382
+ return this.exchangeRates[currency]?.rate ?? 0;
2383
+ }
2384
+ return 0;
2385
+ } catch {
2386
+ return 0;
2387
+ }
2388
+ }
2389
+
2390
+ /**
2391
+ * Attempts to validate a given address.
2392
+ * @param {string} address
2393
+ * @returns {boolean}
2394
+ */
2395
+ public validateAddress(address: string): boolean {
2396
+ return validateAddress({ address, network: this._network }).isValid;
2397
+ }
2398
+
2399
+ /**
2400
+ * Retrieves the next available change address data.
2401
+ * @async
2402
+ * @param {EAddressType} [addressType]
2403
+ * @returns {Promise<Result<IAddress>>}
2404
+ */
2405
+ public async getChangeAddress(
2406
+ addressType = this.data.addressType
2407
+ ): Promise<Result<IAddress>> {
2408
+ const currentWallet = this.data;
2409
+
2410
+ const changeAddressIndexContent =
2411
+ currentWallet.changeAddressIndex[addressType];
2412
+
2413
+ if (
2414
+ changeAddressIndexContent?.address &&
2415
+ changeAddressIndexContent.index >= 0
2416
+ ) {
2417
+ return ok(changeAddressIndexContent);
2418
+ }
2419
+
2420
+ // It's possible we haven't set the change address index yet. Generate one on the fly.
2421
+ const generateAddressResponse = await this.generateAddresses({
2422
+ addressAmount: 0,
2423
+ changeAddressAmount: 1,
2424
+ addressType
2425
+ });
2426
+ if (generateAddressResponse.isErr()) {
2427
+ console.log(generateAddressResponse.error.message);
2428
+ return err('Unable to successfully generate a change address.');
2429
+ }
2430
+ return ok(generateAddressResponse.value.changeAddresses[0]);
2431
+ }
2432
+
2433
+ /**
2434
+ * Returns the current fee estimates for the provided network.
2435
+ * @async
2436
+ * @returns {Promise<IOnchainFees>}
2437
+ */
2438
+ public async getFeeEstimates(network = this._network): Promise<IOnchainFees> {
2439
+ try {
2440
+ if (network === EAvailableNetworks.bitcoinRegtest) {
2441
+ return {
2442
+ fast: 60,
2443
+ normal: 50,
2444
+ slow: 40,
2445
+ minimum: 30,
2446
+ timestamp: Date.now()
2447
+ };
2448
+ }
2449
+
2450
+ const urlModifier = network === 'bitcoin' ? '' : 'testnet/';
2451
+ const response = await fetch(
2452
+ `https://mempool.space/${urlModifier}api/v1/fees/recommended`
2453
+ );
2454
+ const res: IGetFeeEstimatesResponse = await response.json();
2455
+ return {
2456
+ fast: res.fastestFee,
2457
+ normal: res.halfHourFee,
2458
+ slow: res.hourFee,
2459
+ minimum: res.minimumFee,
2460
+ timestamp: Date.now()
2461
+ };
2462
+ } catch {
2463
+ return defaultFeesShape;
2464
+ }
2465
+ }
2466
+
2467
+ /**
2468
+ * Sets up the transaction object with existing inputs and change address information
2469
+ * @async.
2470
+ * @param {ISetupTransaction} params
2471
+ * @returns {TSetupTransactionResponse}
2472
+ */
2473
+ public async setupTransaction(
2474
+ params: ISetupTransaction = {}
2475
+ ): Promise<TSetupTransactionResponse> {
2476
+ return await this.transaction.setupTransaction(params);
2477
+ }
2478
+
2479
+ /**
2480
+ * Returns a fee object for the current transaction.
2481
+ * @param {number} satsPerByte
2482
+ * @param {string} message
2483
+ * @param {Partial<ISendTransaction>} transaction
2484
+ * @param {boolean} fundingLightning
2485
+ * @returns {Result<TGetTotalFeeObj>}
2486
+ */
2487
+ public getFeeInfo({
2488
+ satsPerByte = this.feeEstimates.normal,
2489
+ message = '',
2490
+ transaction,
2491
+ fundingLightning = false
2492
+ }: {
2493
+ satsPerByte?: number;
2494
+ message?: string;
2495
+ transaction?: Partial<ISendTransaction>;
2496
+ fundingLightning?: boolean;
2497
+ } = {}): Result<TGetTotalFeeObj> {
2498
+ return this.transaction.getTotalFeeObj({
2499
+ satsPerByte,
2500
+ message,
2501
+ transaction,
2502
+ fundingLightning
2503
+ });
2504
+ }
2505
+
2506
+ /**
2507
+ * Sets up and creates a transaction to multiple outputs.
2508
+ * @param {ISendTx[]} txs
2509
+ * @param {number} [satsPerByte]
2510
+ * @param {boolean} [rbf]
2511
+ * @param {false} [broadcast]
2512
+ * @param {boolean} [shuffleOutputs]
2513
+ * @returns {Promise<Result<string>>}
2514
+ */
2515
+ public async sendMany({
2516
+ txs = [],
2517
+ satsPerByte = this.feeEstimates.normal,
2518
+ rbf,
2519
+ broadcast = true,
2520
+ shuffleOutputs = true
2521
+ }: {
2522
+ txs: ISendTx[];
2523
+ satsPerByte?: number;
2524
+ rbf?: boolean;
2525
+ broadcast?: boolean;
2526
+ shuffleOutputs?: boolean;
2527
+ }): Promise<Result<string>> {
2528
+ const setupTransactionRes = await this.transaction.setupTransaction({
2529
+ rbf
2530
+ });
2531
+ if (setupTransactionRes.isErr()) {
2532
+ return err(setupTransactionRes.error.message);
2533
+ }
2534
+
2535
+ if (!Array.isArray(txs)) txs = [txs];
2536
+
2537
+ const shuffledTxs = shuffleOutputs ? shuffleArray(txs) : txs;
2538
+ let index = 0;
2539
+ for (const tx of shuffledTxs) {
2540
+ const updateSendTransactionRes = this.transaction.updateSendTransaction({
2541
+ transaction: {
2542
+ label: tx.message,
2543
+ outputs: [{ address: tx.address, value: tx.amount, index }]
2544
+ }
2545
+ });
2546
+ if (updateSendTransactionRes.isErr())
2547
+ err(updateSendTransactionRes.error.message);
2548
+ index++;
2549
+ }
2550
+
2551
+ const updateFeeRes = this.transaction.updateFee({ satsPerByte });
2552
+ if (updateFeeRes.isErr()) {
2553
+ if (updateFeeRes.error.message.includes('Unable to increase the fee')) {
2554
+ const feeInfo = this.getFeeInfo({ satsPerByte: 1 });
2555
+ if (feeInfo.isOk()) {
2556
+ return err(
2557
+ `Fee is too high. The maximum fee for this transaction is ${feeInfo.value.maxSatPerByte}`
2558
+ );
2559
+ }
2560
+ } else {
2561
+ return err(updateFeeRes.error.message);
2562
+ }
2563
+ }
2564
+
2565
+ const createRes = await this.transaction.createTransaction({
2566
+ shuffleOutputs
2567
+ });
2568
+ if (createRes.isErr()) return err(createRes.error.message);
2569
+ const { hex } = createRes.value;
2570
+ if (!broadcast) {
2571
+ return ok(hex);
2572
+ }
2573
+ const broadcastRes = await this.electrum.broadcastTransaction({
2574
+ rawTx: hex
2575
+ });
2576
+ if (broadcastRes.isErr()) return err(broadcastRes.error.message);
2577
+ return ok(broadcastRes.value);
2578
+ }
2579
+
2580
+ /**
2581
+ * Sets up and creates a transaction to a single output/recipient.
2582
+ * @async
2583
+ * @param {string} address
2584
+ * @param {number} amount
2585
+ * @param {string} [message]
2586
+ * @param {number} [satsPerByte]
2587
+ * @param {boolean} [rbf]
2588
+ * @param {boolean} [broadcast]
2589
+ * @param {boolean} [shuffleOutputs]
2590
+ * @returns {Promise<Result<string>>}
2591
+ */
2592
+ public async send({
2593
+ address,
2594
+ amount,
2595
+ message = '',
2596
+ satsPerByte = this.feeEstimates.normal,
2597
+ rbf,
2598
+ broadcast = true,
2599
+ shuffleOutputs = true
2600
+ }: {
2601
+ address: string;
2602
+ amount: number; // sats
2603
+ message?: string;
2604
+ satsPerByte?: number;
2605
+ rbf?: boolean;
2606
+ broadcast?: boolean;
2607
+ shuffleOutputs?: boolean;
2608
+ }): Promise<Result<string>> {
2609
+ return await this.sendMany({
2610
+ txs: [{ address, amount, message }],
2611
+ satsPerByte,
2612
+ rbf,
2613
+ broadcast,
2614
+ shuffleOutputs
2615
+ });
2616
+ }
2617
+
2618
+ /**
2619
+ * Returns the address from a provided script hash in storage.
2620
+ * @param {string} scriptHash
2621
+ * @returns {IAddress | undefined}
2622
+ */
2623
+ public getAddressFromScriptHash(scriptHash: string): IAddress | undefined {
2624
+ const addresses: TAddressTypeContent<IAddresses> = this.data.addresses;
2625
+ const changeAddresses: TAddressTypeContent<IAddresses> =
2626
+ this.data.changeAddresses;
2627
+ const combinedAddresses: IAddress[] = [
2628
+ ...Object.values(addresses),
2629
+ ...Object.values(changeAddresses)
2630
+ ].flatMap((addressGroup: IAddresses) => Object.values(addressGroup));
2631
+ return combinedAddresses.find(
2632
+ (addressObj: IAddress) => addressObj.scriptHash === scriptHash
2633
+ );
2634
+ }
2635
+
2636
+ /**
2637
+ * Will ensure that both address and change address indexes are set at index 0.
2638
+ * Will also generate and store address and changeAddress at index 0.
2639
+ * @private
2640
+ * @async
2641
+ * @returns {Promise<Result<string>>}
2642
+ */
2643
+ private async setZeroIndexAddresses(): Promise<Result<string>> {
2644
+ const types = Object.values(EAddressType);
2645
+ const currentWallet = this.data;
2646
+ let saveAddressIndexes = false;
2647
+ let saveChangeAddressIndexes = false;
2648
+
2649
+ for (const addressType of types) {
2650
+ const addressIndex = currentWallet.addressIndex[addressType];
2651
+ const changeAddressIndex = currentWallet.changeAddressIndex[addressType];
2652
+
2653
+ if (addressIndex?.index < 0) {
2654
+ await this.updateAddressIndex(addressType, false);
2655
+ saveAddressIndexes = true;
2656
+ }
2657
+ if (changeAddressIndex?.index < 0) {
2658
+ await this.updateAddressIndex(addressType, true);
2659
+ saveChangeAddressIndexes = true;
2660
+ }
2661
+ }
2662
+
2663
+ if (saveAddressIndexes) {
2664
+ await this.saveWalletData('addressIndex', this._data.addressIndex);
2665
+ }
2666
+ if (saveChangeAddressIndexes) {
2667
+ await this.saveWalletData(
2668
+ 'changeAddressIndex',
2669
+ this._data.changeAddressIndex
2670
+ );
2671
+ }
2672
+
2673
+ return ok('Set Zero Index Addresses.');
2674
+ }
2675
+
2676
+ /**
2677
+ * Updates the address index for a given address type.
2678
+ * @private
2679
+ * @async
2680
+ * @param {EAddressType} addressType
2681
+ * @param {boolean} isChangeAddress
2682
+ * @param {number} [index]
2683
+ * @returns {Promise<void>}
2684
+ */
2685
+ private async updateAddressIndex(
2686
+ addressType: EAddressType,
2687
+ isChangeAddress: boolean,
2688
+ index = 0
2689
+ ): Promise<void> {
2690
+ const address = await this.generateAddresses({
2691
+ addressAmount: isChangeAddress ? 0 : 1,
2692
+ addressIndex: index,
2693
+ changeAddressAmount: isChangeAddress ? 1 : 0,
2694
+ changeAddressIndex: index,
2695
+ addressType
2696
+ });
2697
+ const indexToUpdate = isChangeAddress
2698
+ ? 'changeAddressIndex'
2699
+ : 'addressIndex';
2700
+ if (address.isOk()) {
2701
+ const key = isChangeAddress ? 'changeAddresses' : 'addresses';
2702
+ this._data[indexToUpdate][addressType] =
2703
+ address.value[key][Object.keys(address.value[key])[0]];
2704
+ }
2705
+ // Ensure we have addresses to pull from.
2706
+ let addresses = {};
2707
+ if (indexToUpdate === 'addressIndex') {
2708
+ addresses = this.data.addresses[addressType];
2709
+ } else {
2710
+ addresses = this.data.changeAddresses[addressType];
2711
+ }
2712
+ if (!Object.keys(addresses).length) {
2713
+ await this.addAddresses({
2714
+ addressAmount:
2715
+ indexToUpdate === 'addressIndex' ? GENERATE_ADDRESS_AMOUNT : 0,
2716
+ addressIndex: index,
2717
+ changeAddressAmount:
2718
+ indexToUpdate === 'changeAddressIndex' ? GENERATE_ADDRESS_AMOUNT : 0,
2719
+ changeAddressIndex: index,
2720
+ addressType
2721
+ });
2722
+ }
2723
+ }
2724
+
2725
+ /**
2726
+ * Returns current address index information.
2727
+ * @returns {TAddressIndexInfo}
2728
+ */
2729
+ public getAddressIndexInfo(): TAddressIndexInfo {
2730
+ const addressType = this.data.addressType;
2731
+ const currentWallet = this.data;
2732
+ const addressIndex = currentWallet.addressIndex[addressType];
2733
+ const changeAddressIndex = currentWallet.addressIndex[addressType];
2734
+ const lastUsedAddressIndex =
2735
+ currentWallet.lastUsedAddressIndex[addressType];
2736
+ const lastUsedChangeAddressIndex =
2737
+ currentWallet.lastUsedChangeAddressIndex[addressType];
2738
+ return {
2739
+ addressIndex,
2740
+ changeAddressIndex,
2741
+ lastUsedAddressIndex,
2742
+ lastUsedChangeAddressIndex
2743
+ };
2744
+ }
2745
+
2746
+ /**
2747
+ * Returns the next available receive address.
2748
+ * @param {EAddressType} [addressType]
2749
+ * @returns {Promise<Result<string>>}
2750
+ */
2751
+ getReceiveAddress = async ({
2752
+ addressType = this.data.addressType
2753
+ }: {
2754
+ addressType?: EAddressType;
2755
+ }): Promise<Result<string>> => {
2756
+ try {
2757
+ const wallet = this.data;
2758
+ const addressIndex = wallet.addressIndex;
2759
+ const receiveAddress = addressIndex[addressType].address;
2760
+ if (receiveAddress) {
2761
+ return ok(receiveAddress);
2762
+ }
2763
+ const addresses = wallet?.addresses[addressType];
2764
+
2765
+ // Check if addresses were generated, but the index has not been set yet.
2766
+ if (
2767
+ Object.keys(addresses).length > 0 &&
2768
+ addressIndex[addressType].index < 0
2769
+ ) {
2770
+ // Grab and return the address at index 0.
2771
+ const address = Object.values(addresses).find(
2772
+ ({ index }) => index === 0
2773
+ );
2774
+ if (address) {
2775
+ return ok(address.address);
2776
+ }
2777
+ }
2778
+ // Fallback to generating a new receive address on the fly.
2779
+ const generatedAddress = await this.generateNewReceiveAddress({
2780
+ addressType
2781
+ });
2782
+ if (generatedAddress.isOk()) {
2783
+ return ok(generatedAddress.value.address);
2784
+ } else {
2785
+ console.log(generatedAddress.error.message);
2786
+ }
2787
+ return err('No receive address available.');
2788
+ } catch (e) {
2789
+ return err(e);
2790
+ }
2791
+ };
2792
+
2793
+ /**
2794
+ * Using a tx_hash this method will return the necessary data to create a
2795
+ * replace-by-fee transaction for any 0-conf, RBF-enabled tx.
2796
+ * @param {ITxHash} txHash
2797
+ * @returns {Promise<Result<IRbfData>>}
2798
+ */
2799
+ public async getRbfData({
2800
+ txHash
2801
+ }: {
2802
+ txHash: ITxHash;
2803
+ }): Promise<Result<IRbfData>> {
2804
+ const txResponse = await this.electrum.getTransactions({
2805
+ txHashes: [txHash]
2806
+ });
2807
+ if (txResponse.isErr()) {
2808
+ return err(txResponse.error.message);
2809
+ }
2810
+ const txData = txResponse.value.data;
2811
+
2812
+ const wallet = this.data;
2813
+ const addressTypeKeys = objectKeys(EAddressType);
2814
+ const addresses = wallet.addresses;
2815
+ const changeAddresses = wallet.changeAddresses;
2816
+
2817
+ let allAddresses = {} as IAddresses;
2818
+ let allChangeAddresses = {} as IAddresses;
2819
+
2820
+ await Promise.all(
2821
+ addressTypeKeys.map((addressType) => {
2822
+ allAddresses = {
2823
+ ...allAddresses,
2824
+ ...addresses[addressType],
2825
+ ...changeAddresses[addressType]
2826
+ };
2827
+ allChangeAddresses = {
2828
+ ...allChangeAddresses,
2829
+ ...changeAddresses[addressType]
2830
+ };
2831
+ })
2832
+ );
2833
+
2834
+ let changeAddressData: IOutput = {
2835
+ address: '',
2836
+ value: 0,
2837
+ index: 0
2838
+ };
2839
+ const inputs: IUtxo[] = [];
2840
+ let address = '';
2841
+ let scriptHash = '';
2842
+ let path = '';
2843
+ let value = 0;
2844
+ const addressType = EAddressType.p2wpkh;
2845
+ const outputs: IOutput[] = [];
2846
+ let message = '';
2847
+ let inputTotal = 0;
2848
+ let outputTotal = 0;
2849
+ let fee = 0;
2850
+
2851
+ const insAndOuts = await Promise.all(
2852
+ txData.map(({ result }) => {
2853
+ const vin = result.vin ?? [];
2854
+ const vout = result.vout ?? [];
2855
+ return { vins: vin, vouts: vout };
2856
+ })
2857
+ );
2858
+ const { vins, vouts } = insAndOuts[0];
2859
+ for (let i = 0; i < vins.length; i++) {
2860
+ try {
2861
+ const input = vins[i];
2862
+ const txId = input.txid;
2863
+ const tx = await this.electrum.getTransactions({
2864
+ txHashes: [{ tx_hash: txId }]
2865
+ });
2866
+ if (tx.isErr()) {
2867
+ return err(tx.error.message);
2868
+ }
2869
+ if (tx.value.data[0].data.height > 0) {
2870
+ return err('Transaction is already confirmed. Unable to RBF.');
2871
+ }
2872
+ const txVout = tx.value.data[0].result.vout[input.vout];
2873
+ if (txVout.scriptPubKey?.address) {
2874
+ address = txVout.scriptPubKey.address;
2875
+ } else if (
2876
+ txVout.scriptPubKey?.addresses &&
2877
+ txVout.scriptPubKey.addresses.length
2878
+ ) {
2879
+ address = txVout.scriptPubKey.addresses[0];
2880
+ }
2881
+ if (!address) {
2882
+ continue;
2883
+ }
2884
+ scriptHash = getScriptHash({ address, network: this._network });
2885
+ // Check that we are in possession of this scriptHash.
2886
+ if (!(scriptHash in allAddresses)) {
2887
+ // This output did not come from us.
2888
+ continue;
2889
+ }
2890
+ path = allAddresses[scriptHash].path;
2891
+ value = btcToSats(txVout.value);
2892
+ inputs.push({
2893
+ tx_hash: input.txid,
2894
+ index: input.vout,
2895
+ tx_pos: input.vout,
2896
+ height: 0,
2897
+ address,
2898
+ scriptHash,
2899
+ path,
2900
+ value
2901
+ });
2902
+ if (value) {
2903
+ inputTotal = inputTotal + value;
2904
+ }
2905
+ } catch (e) {
2906
+ console.log(e);
2907
+ }
2908
+ }
2909
+ for (let i = 0; i < vouts.length; i++) {
2910
+ const vout = vouts[i];
2911
+ const voutValue = btcToSats(vout.value);
2912
+ if (vout.scriptPubKey?.addresses) {
2913
+ address = vout.scriptPubKey.addresses[0];
2914
+ } else if (vout.scriptPubKey?.address) {
2915
+ address = vout.scriptPubKey.address;
2916
+ } else {
2917
+ try {
2918
+ if (vout.scriptPubKey.asm.includes('OP_RETURN')) {
2919
+ message = decodeOpReturnMessage(vout.scriptPubKey.asm)[0] || '';
2920
+ }
2921
+ } catch {}
2922
+ }
2923
+ if (!address) {
2924
+ continue;
2925
+ }
2926
+ const changeAddressScriptHash = await getScriptHash({
2927
+ address,
2928
+ network: this._network
2929
+ });
2930
+
2931
+ // If the address scripthash matches one of our address scripthashes, add it accordingly. Otherwise, add it as another output.
2932
+ if (Object.keys(allAddresses).includes(changeAddressScriptHash)) {
2933
+ changeAddressData = {
2934
+ address,
2935
+ value: voutValue,
2936
+ index: i
2937
+ };
2938
+ }
2939
+ const index = outputs?.length ?? 0;
2940
+ outputs.push({
2941
+ address,
2942
+ value: voutValue,
2943
+ index
2944
+ });
2945
+ outputTotal = outputTotal + voutValue;
2946
+ }
2947
+
2948
+ if (!changeAddressData?.address && outputs.length >= 2) {
2949
+ /*
2950
+ * Unable to determine change address.
2951
+ * Performing an RBF could divert funds from the incorrect output.
2952
+ *
2953
+ * It's very possible that this tx sent the max amount of sats to a foreign/unknown address.
2954
+ * Instead of pulling sats from that output to accommodate the higher fee (reducing how much the recipient receives)
2955
+ * suggest a CPFP transaction.
2956
+ */
2957
+ return err('cpfp');
2958
+ }
2959
+
2960
+ if (outputTotal > inputTotal) {
2961
+ return err('Outputs should not be greater than the inputs.');
2962
+ }
2963
+ fee = Math.abs(Number(inputTotal - outputTotal));
2964
+
2965
+ return ok({
2966
+ changeAddress: changeAddressData.address,
2967
+ inputs,
2968
+ balance: inputTotal,
2969
+ outputs,
2970
+ fee,
2971
+ message,
2972
+ addressType,
2973
+ rbf: true
2974
+ });
2975
+ }
2976
+
2977
+ /**
2978
+ * Deletes a given on-chain transaction by id.
2979
+ * @param {string} txid
2980
+ */
2981
+ async deleteOnChainTransactionById({
2982
+ txid
2983
+ }: {
2984
+ txid: string;
2985
+ }): Promise<void> {
2986
+ const transactions = this.data.transactions;
2987
+ if (txid in transactions) {
2988
+ delete transactions[txid];
2989
+ }
2990
+ await this.saveWalletData('transactions', transactions);
2991
+ }
2992
+
2993
+ /**
2994
+ * Sets "exists" to false for a given on-chain transaction id.
2995
+ * @param {string} txid
2996
+ */
2997
+ async addGhostTransaction({ txid }: { txid: string }): Promise<void> {
2998
+ if (txid in this._data.transactions) {
2999
+ this._data.transactions[txid].exists = false;
3000
+ }
3001
+ await this.saveWalletData('transactions', this._data.transactions);
3002
+ }
3003
+
3004
+ /**
3005
+ * Adds a boosted transaction id to the boostedTransactions object.
3006
+ * @param {string} newTxId
3007
+ * @param {string} oldTxId
3008
+ * @param {EBoostType} [type]
3009
+ * @param {number} fee
3010
+ * @returns {Promise<Result<IBoostedTransaction>>}
3011
+ */
3012
+ async addBoostedTransaction({
3013
+ newTxId,
3014
+ oldTxId,
3015
+ type = EBoostType.cpfp,
3016
+ fee
3017
+ }: {
3018
+ newTxId: string;
3019
+ oldTxId: string;
3020
+ type?: EBoostType;
3021
+ fee: number;
3022
+ }): Promise<Result<IBoostedTransaction>> {
3023
+ try {
3024
+ const boostedTransactions = this.data.boostedTransactions;
3025
+ const parentTransactions = this.getBoostedTransactionParents({
3026
+ txid: oldTxId,
3027
+ boostedTransactions
3028
+ });
3029
+ parentTransactions.push(oldTxId);
3030
+ const boostedTx: IBoostedTransaction = {
3031
+ parentTransactions: parentTransactions,
3032
+ childTransaction: newTxId,
3033
+ type,
3034
+ fee
3035
+ };
3036
+ const boostedTransaction: IBoostedTransactions = {
3037
+ [oldTxId]: boostedTx
3038
+ };
3039
+ this._data.boostedTransactions = {
3040
+ ...this.data.boostedTransactions,
3041
+ ...boostedTransaction
3042
+ };
3043
+ await this.saveWalletData(
3044
+ 'boostedTransactions',
3045
+ this._data.boostedTransactions
3046
+ );
3047
+ return ok(boostedTx);
3048
+ } catch (e) {
3049
+ return err(e);
3050
+ }
3051
+ }
3052
+
3053
+ /**
3054
+ * Returns an array of parents for a boosted transaction id.
3055
+ * @param {string} txid
3056
+ * @param {IBoostedTransactions} [boostedTransactions]
3057
+ * @returns {string[]}
3058
+ */
3059
+ getBoostedTransactionParents = ({
3060
+ txid,
3061
+ boostedTransactions
3062
+ }: {
3063
+ txid: string;
3064
+ boostedTransactions?: IBoostedTransactions;
3065
+ }): string[] => {
3066
+ if (!boostedTransactions) {
3067
+ boostedTransactions = this.getBoostedTransactions();
3068
+ }
3069
+ const boostObj = Object.values(boostedTransactions).find((boostObject) => {
3070
+ return boostObject.childTransaction === txid;
3071
+ });
3072
+
3073
+ return boostObj?.parentTransactions ?? [];
3074
+ };
3075
+
3076
+ /**
3077
+ * Returns boosted transactions object.
3078
+ * @returns {IBoostedTransactions}
3079
+ */
3080
+ getBoostedTransactions = (): IBoostedTransactions => {
3081
+ return this.data.boostedTransactions;
3082
+ };
3083
+
3084
+ /**
3085
+ * This completely resets the send transaction state.
3086
+ * @returns {Promise<Result<string>>}
3087
+ */
3088
+ async resetSendTransaction(): Promise<Result<string>> {
3089
+ return await this.transaction.resetSendTransaction();
3090
+ }
3091
+
3092
+ /**
3093
+ * Returns an array of transactions that can be boosted with cpfp and rbf.
3094
+ * @returns {{cpfp: IFormattedTransaction[], rbf: IFormattedTransaction[]}}
3095
+ */
3096
+ getBoostableTransactions(): {
3097
+ cpfp: IFormattedTransaction[];
3098
+ rbf: IFormattedTransaction[];
3099
+ } {
3100
+ const cpfp: IFormattedTransaction[] = [];
3101
+ const rbf: IFormattedTransaction[] = [];
3102
+ Object.values(this.data.unconfirmedTransactions).map((tx) => {
3103
+ if (tx.rbf) rbf.push(tx);
3104
+ cpfp.push(tx); // All unconfirmed transactions can be cpfp'd.
3105
+ });
3106
+ return { cpfp, rbf };
3107
+ }
3108
+
3109
+ /**
3110
+ * Creates a BIP32Interface from the selected wallet's mnemonic and passphrase
3111
+ * @returns {Promise<Result<BIP32Interface>>}
3112
+ */
3113
+ getBip32Interface = async (): Promise<Result<BIP32Interface>> => {
3114
+ const network = networks[this._network];
3115
+ const seed = await bip39.mnemonicToSeed(this._mnemonic, this._passphrase);
3116
+ const root = bip32.fromSeed(seed, network);
3117
+ return ok(root);
3118
+ };
3119
+
3120
+ /**
3121
+ * Adds a specified input to the current transaction.
3122
+ * @param {IUtxo} input
3123
+ * @returns {Result<IUtxo[]>}
3124
+ */
3125
+ public addTxInput({ input }: { input: IUtxo }): Result<IUtxo[]> {
3126
+ try {
3127
+ const txData = this.transaction.data;
3128
+ const inputs = txData?.inputs ?? [];
3129
+ const newInputs = [...inputs, input];
3130
+ this.transaction.updateSendTransaction({
3131
+ transaction: {
3132
+ inputs: newInputs
3133
+ }
3134
+ });
3135
+ return ok(newInputs);
3136
+ } catch (e) {
3137
+ console.log(e);
3138
+ return err(e);
3139
+ }
3140
+ }
3141
+
3142
+ /**
3143
+ * Removes the specified input from the current transaction.
3144
+ * @param {IUtxo} input
3145
+ * @returns {Result<IUtxo[]>}
3146
+ */
3147
+ public removeTxInput({ input }: { input: IUtxo }): Result<IUtxo[]> {
3148
+ try {
3149
+ const txData = this.transaction.data;
3150
+ const txInputs = txData?.inputs ?? [];
3151
+ const newInputs = txInputs.filter((txInput) => {
3152
+ if (!objectsMatch(input, txInput)) {
3153
+ return txInput;
3154
+ }
3155
+ });
3156
+ this.transaction.updateSendTransaction({
3157
+ transaction: {
3158
+ inputs: newInputs
3159
+ }
3160
+ });
3161
+ return ok(newInputs);
3162
+ } catch (e) {
3163
+ console.log(e);
3164
+ return err(e);
3165
+ }
3166
+ }
3167
+
3168
+ /**
3169
+ * Adds a specified tag to the current transaction.
3170
+ * @param {string} tag
3171
+ * @returns {Result<string>}
3172
+ */
3173
+ addTxTag({ tag }: { tag: string }): Result<string> {
3174
+ try {
3175
+ const txData = this.transaction.data;
3176
+ let tags = [...txData.tags, tag];
3177
+ tags = [...new Set(tags)]; // remove duplicates
3178
+ this.transaction.updateSendTransaction({
3179
+ transaction: {
3180
+ ...txData,
3181
+ tags
3182
+ }
3183
+ });
3184
+ return ok('Tag successfully added');
3185
+ } catch (e) {
3186
+ console.log(e);
3187
+ return err(e);
3188
+ }
3189
+ }
3190
+
3191
+ /**
3192
+ * Removes a specified tag from the current transaction.
3193
+ * @param {string} tag
3194
+ * @returns {Result<string>}
3195
+ */
3196
+ removeTxTag({ tag }: { tag: string }): Result<string> {
3197
+ try {
3198
+ const txData = this.transaction.data;
3199
+ const tags = txData.tags;
3200
+ const newTags = tags.filter((t) => t !== tag);
3201
+
3202
+ this.transaction.updateSendTransaction({
3203
+ transaction: {
3204
+ ...txData,
3205
+ tags: newTags
3206
+ }
3207
+ });
3208
+ return ok('Tag successfully added');
3209
+ } catch (e) {
3210
+ console.log(e);
3211
+ return err(e);
3212
+ }
3213
+ }
3214
+
3215
+ /**
3216
+ * Updates the fee rate for the current transaction to the preferred value if none set.
3217
+ * @param {number} [satsPerByte]
3218
+ * @param {EFeeId} [selectedFeeId]
3219
+ * @returns {Result<string>}
3220
+ */
3221
+ setupFeeForOnChainTransaction({
3222
+ satsPerByte,
3223
+ selectedFeeId
3224
+ }: {
3225
+ satsPerByte?: number;
3226
+ selectedFeeId?: EFeeId;
3227
+ }): Result<string> {
3228
+ try {
3229
+ const transactionData = this.transaction.data;
3230
+ if (!satsPerByte) {
3231
+ satsPerByte = transactionData?.satsPerByte ?? 1;
3232
+ satsPerByte =
3233
+ this.selectedFeeId === EFeeId.none
3234
+ ? satsPerByte
3235
+ : this.feeEstimates[this.selectedFeeId];
3236
+ }
3237
+ const res = this.transaction.updateFee({
3238
+ satsPerByte: satsPerByte ?? 1,
3239
+ selectedFeeId
3240
+ });
3241
+ if (res.isErr()) {
3242
+ console.log(res.error.message);
3243
+ return err(res.error.message);
3244
+ }
3245
+
3246
+ return ok('Fee has been changed successfully');
3247
+ } catch (e) {
3248
+ return err(e);
3249
+ }
3250
+ }
3251
+
3252
+ /**
3253
+ * Used to temporarily update the balance until the Electrum server catches up after sending a transaction.
3254
+ * @param {number} balance
3255
+ * @returns {Result<string>}
3256
+ */
3257
+ public updateWalletBalance({ balance }: { balance: number }): Result<string> {
3258
+ try {
3259
+ this._data.balance = balance;
3260
+ this.saveWalletData('balance', balance);
3261
+ return ok('Successfully updated balance.');
3262
+ } catch (e) {
3263
+ return err(e);
3264
+ }
3265
+ }
3266
+
3267
+ /**
3268
+ * Updates the exchange rates for the current network.
3269
+ * @public
3270
+ * @async
3271
+ * @returns {Promise<Result<IExchangeRates>>}
3272
+ */
3273
+ public async updateExchangeRates(): Promise<Result<IExchangeRates>> {
3274
+ const exchangeRates = await getExchangeRates();
3275
+ if (exchangeRates.isErr()) return err(exchangeRates.error.message);
3276
+ this.exchangeRates = exchangeRates.value;
3277
+ await this.saveWalletData('exchangeRates', this.exchangeRates);
3278
+ return ok(this.exchangeRates);
3279
+ }
3280
+
3281
+ /**
3282
+ * Updates the fee estimates for the current network.
3283
+ * @public
3284
+ * @async
3285
+ * @param {true} [forceUpdate] Ignores the timestamp if set true and forces the update
3286
+ * @returns {Promise<Result<IOnchainFees>>}
3287
+ */
3288
+ public async updateFeeEstimates(
3289
+ forceUpdate = false
3290
+ ): Promise<Result<IOnchainFees>> {
3291
+ const timestamp = this.feeEstimates.timestamp;
3292
+ const difference = Math.floor((Date.now() - timestamp) / 1000);
3293
+ if (!forceUpdate && difference < 1800) {
3294
+ return ok(this.feeEstimates);
3295
+ }
3296
+ const feeEstimates = await this.getFeeEstimates();
3297
+ if (!feeEstimates) {
3298
+ return err('Unable to retrieve fee estimates.');
3299
+ }
3300
+ this.feeEstimates = feeEstimates;
3301
+ await this.saveWalletData('feeEstimates', feeEstimates);
3302
+ return ok(feeEstimates);
3303
+ }
3304
+ }