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.
- package/.eslintignore +4 -0
- package/.eslintrc +40 -0
- package/.prettierrc +9 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/electrum/constants.js +31 -0
- package/dist/electrum/constants.js.map +1 -0
- package/dist/electrum/index.js +552 -0
- package/dist/electrum/index.js.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/shapes/electrum.js +14 -0
- package/dist/shapes/electrum.js.map +1 -0
- package/dist/shapes/index.js +19 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/shapes/wallet.js +115 -0
- package/dist/shapes/wallet.js.map +1 -0
- package/dist/transaction/index.js +692 -0
- package/dist/transaction/index.js.map +1 -0
- package/dist/types/electrum/constants.d.ts +7 -0
- package/dist/types/electrum/index.d.ts +85 -0
- package/dist/types/electrum.js +9 -0
- package/dist/types/electrum.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +20 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/shapes/electrum.d.ts +5 -0
- package/dist/types/shapes/index.d.ts +2 -0
- package/dist/types/shapes/wallet.d.ts +10 -0
- package/dist/types/transaction/index.d.ts +92 -0
- package/dist/types/transaction.js +12 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/types/types/electrum.d.ts +113 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/types/transaction.d.ts +51 -0
- package/dist/types/types/wallet.d.ts +400 -0
- package/dist/types/utils/conversion.d.ts +10 -0
- package/dist/types/utils/derivation-path.d.ts +19 -0
- package/dist/types/utils/electrum.d.ts +10 -0
- package/dist/types/utils/exchange-rate.d.ts +30 -0
- package/dist/types/utils/helpers.d.ts +23 -0
- package/dist/types/utils/index.d.ts +7 -0
- package/dist/types/utils/result.d.ts +16 -0
- package/dist/types/utils/transaction.d.ts +21 -0
- package/dist/types/utils/wallet.d.ts +47 -0
- package/dist/types/wallet/constants.d.ts +12 -0
- package/dist/types/wallet/index.d.ts +214 -0
- package/dist/types/wallet.js +36 -0
- package/dist/types/wallet.js.map +1 -0
- package/dist/utils/conversion.js +52 -0
- package/dist/utils/conversion.js.map +1 -0
- package/dist/utils/derivation-path.js +104 -0
- package/dist/utils/derivation-path.js.map +1 -0
- package/dist/utils/electrum.js +149 -0
- package/dist/utils/electrum.js.map +1 -0
- package/dist/utils/exchange-rate.js +49 -0
- package/dist/utils/exchange-rate.js.map +1 -0
- package/dist/utils/helpers.js +137 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/result.js +36 -0
- package/dist/utils/result.js.map +1 -0
- package/dist/utils/transaction.js +327 -0
- package/dist/utils/transaction.js.map +1 -0
- package/dist/utils/wallet.js +227 -0
- package/dist/utils/wallet.js.map +1 -0
- package/dist/wallet/constants.js +15 -0
- package/dist/wallet/constants.js.map +1 -0
- package/dist/wallet/index.js +2113 -0
- package/dist/wallet/index.js.map +1 -0
- package/docs/html/.nojekyll +1 -0
- package/docs/html/assets/highlight.css +22 -0
- package/docs/html/assets/main.js +58 -0
- package/docs/html/assets/search.js +1 -0
- package/docs/html/assets/style.css +1367 -0
- package/docs/html/classes/Electrum.html +812 -0
- package/docs/html/classes/Transaction.html +687 -0
- package/docs/html/classes/Wallet.html +2030 -0
- package/docs/html/enums/EAddressType.html +227 -0
- package/docs/html/enums/EAvailableNetworks.html +255 -0
- package/docs/html/enums/EBoostType.html +220 -0
- package/docs/html/enums/EFeeId.html +241 -0
- package/docs/html/enums/EPaymentType.html +220 -0
- package/docs/html/enums/EProtocol.html +220 -0
- package/docs/html/enums/EUnit.html +227 -0
- package/docs/html/functions/availableNetworks.html +200 -0
- package/docs/html/functions/constructByteCountParam.html +210 -0
- package/docs/html/functions/decodeOpReturnMessage.html +202 -0
- package/docs/html/functions/decodeRawTransaction.html +210 -0
- package/docs/html/functions/err.html +214 -0
- package/docs/html/functions/formatKeyDerivationPath.html +219 -0
- package/docs/html/functions/formatPeerData.html +207 -0
- package/docs/html/functions/generateMnemonic.html +223 -0
- package/docs/html/functions/generateWalletId.html +202 -0
- package/docs/html/functions/getAddressFromScriptPubKey.html +209 -0
- package/docs/html/functions/getAddressTypeFromPath.html +206 -0
- package/docs/html/functions/getByteCount.html +206 -0
- package/docs/html/functions/getDataFallback.html +207 -0
- package/docs/html/functions/getDefaultPort.html +209 -0
- package/docs/html/functions/getDefaultWalletData.html +200 -0
- package/docs/html/functions/getDefaultWalletDataKeys.html +200 -0
- package/docs/html/functions/getExchangeRates.html +200 -0
- package/docs/html/functions/getHighestUsedIndexFromTxHashes.html +216 -0
- package/docs/html/functions/getKeyDerivationPath.html +211 -0
- package/docs/html/functions/getKeyDerivationPathObject.html +216 -0
- package/docs/html/functions/getKeyDerivationPathString.html +220 -0
- package/docs/html/functions/getKeyValue.html +206 -0
- package/docs/html/functions/getPeers.html +211 -0
- package/docs/html/functions/getProtocolForPort.html +209 -0
- package/docs/html/functions/getScriptHash.html +210 -0
- package/docs/html/functions/getSeed.html +209 -0
- package/docs/html/functions/getSeedHash.html +206 -0
- package/docs/html/functions/getSha256.html +206 -0
- package/docs/html/functions/getStorageKeyValues.html +209 -0
- package/docs/html/functions/getTxFee.html +210 -0
- package/docs/html/functions/getWalletDataStorageKey.html +206 -0
- package/docs/html/functions/isValidBech32mEncodedString.html +211 -0
- package/docs/html/functions/objectKeys-1.html +211 -0
- package/docs/html/functions/objectsMatch.html +210 -0
- package/docs/html/functions/ok.html +214 -0
- package/docs/html/functions/parseOnChainPaymentRequest.html +204 -0
- package/docs/html/functions/reduceValue.html +215 -0
- package/docs/html/functions/removeDustOutputs.html +206 -0
- package/docs/html/functions/setReplaceByFee.html +210 -0
- package/docs/html/functions/shuffleArray.html +211 -0
- package/docs/html/functions/validateAddress.html +216 -0
- package/docs/html/functions/validateMnemonic.html +206 -0
- package/docs/html/functions/validateTransaction.html +206 -0
- package/docs/html/index.html +360 -0
- package/docs/html/interfaces/IAddInput.html +231 -0
- package/docs/html/interfaces/IAddress.html +247 -0
- package/docs/html/interfaces/IAddressData.html +231 -0
- package/docs/html/interfaces/IAddressType.html +199 -0
- package/docs/html/interfaces/IAddressTypeData.html +252 -0
- package/docs/html/interfaces/IAddresses.html +199 -0
- package/docs/html/interfaces/IBoostedTransaction.html +238 -0
- package/docs/html/interfaces/IBoostedTransactions.html +199 -0
- package/docs/html/interfaces/ICreateTransaction.html +224 -0
- package/docs/html/interfaces/ICustomGetAddress.html +231 -0
- package/docs/html/interfaces/ICustomGetScriptHash.html +224 -0
- package/docs/html/interfaces/IElectrumGetAddressBalanceRes.html +235 -0
- package/docs/html/interfaces/IExchangeRates.html +210 -0
- package/docs/html/interfaces/IFormattedPeerData.html +245 -0
- package/docs/html/interfaces/IFormattedTransaction.html +343 -0
- package/docs/html/interfaces/IFormattedTransactions.html +199 -0
- package/docs/html/interfaces/IGenerateAddresses.html +252 -0
- package/docs/html/interfaces/IGenerateAddressesResponse.html +224 -0
- package/docs/html/interfaces/IGetAddress.html +231 -0
- package/docs/html/interfaces/IGetAddressBalanceRes.html +226 -0
- package/docs/html/interfaces/IGetAddressByPath.html +224 -0
- package/docs/html/interfaces/IGetAddressHistoryResponse.html +269 -0
- package/docs/html/interfaces/IGetAddressResponse.html +231 -0
- package/docs/html/interfaces/IGetAddressScriptHashesHistoryResponse.html +245 -0
- package/docs/html/interfaces/IGetDerivationPath.html +259 -0
- package/docs/html/interfaces/IGetFeeEstimatesResponse.html +238 -0
- package/docs/html/interfaces/IGetHeaderResponse.html +245 -0
- package/docs/html/interfaces/IGetNextAvailableAddressResponse.html +238 -0
- package/docs/html/interfaces/IGetTransactions.html +245 -0
- package/docs/html/interfaces/IGetTransactionsFromInputs.html +245 -0
- package/docs/html/interfaces/IGetUtxosResponse.html +224 -0
- package/docs/html/interfaces/IHeader.html +231 -0
- package/docs/html/interfaces/IIndexes.html +238 -0
- package/docs/html/interfaces/IKeyDerivationPath.html +247 -0
- package/docs/html/interfaces/IKeyDerivationPathData.html +224 -0
- package/docs/html/interfaces/INewBlock.html +224 -0
- package/docs/html/interfaces/IOnchainFees.html +245 -0
- package/docs/html/interfaces/IOutput.html +231 -0
- package/docs/html/interfaces/IPeerData.html +231 -0
- package/docs/html/interfaces/IRbfData.html +252 -0
- package/docs/html/interfaces/ISend.html +224 -0
- package/docs/html/interfaces/ISendTransaction.html +322 -0
- package/docs/html/interfaces/ISendTx.html +231 -0
- package/docs/html/interfaces/ISetupTransaction.html +238 -0
- package/docs/html/interfaces/ISubscribeToAddress.html +247 -0
- package/docs/html/interfaces/ISubscribeToHeader.html +245 -0
- package/docs/html/interfaces/ITargets.html +238 -0
- package/docs/html/interfaces/ITransaction.html +281 -0
- package/docs/html/interfaces/ITxHash.html +217 -0
- package/docs/html/interfaces/ITxHashes.html +235 -0
- package/docs/html/interfaces/IUtxo.html +275 -0
- package/docs/html/interfaces/IVin.html +252 -0
- package/docs/html/interfaces/IVout.html +246 -0
- package/docs/html/interfaces/IWallet.html +350 -0
- package/docs/html/interfaces/IWalletData.html +343 -0
- package/docs/html/types/ElectrumConnectionPubSub.html +233 -0
- package/docs/html/types/ElectrumConnectionSubscription.html +204 -0
- package/docs/html/types/InputData.html +203 -0
- package/docs/html/types/ObjectKeys.html +198 -0
- package/docs/html/types/Result.html +201 -0
- package/docs/html/types/TAddressIndexInfo.html +204 -0
- package/docs/html/types/TAddressLabel.html +193 -0
- package/docs/html/types/TAddressType.html +193 -0
- package/docs/html/types/TAddressTypeContent.html +198 -0
- package/docs/html/types/TAddressTypes.html +193 -0
- package/docs/html/types/TAvailableNetworks.html +193 -0
- package/docs/html/types/TConnectToElectrumRes.html +193 -0
- package/docs/html/types/TDecodeRawTx.html +214 -0
- package/docs/html/types/TElectrumNetworks.html +193 -0
- package/docs/html/types/TGetAddressHistory.html +200 -0
- package/docs/html/types/TGetByteCountInput.html +193 -0
- package/docs/html/types/TGetByteCountInputs.html +193 -0
- package/docs/html/types/TGetByteCountOutput.html +193 -0
- package/docs/html/types/TGetByteCountOutputs.html +193 -0
- package/docs/html/types/TGetData.html +211 -0
- package/docs/html/types/TGetTotalFeeObj.html +204 -0
- package/docs/html/types/TKeyDerivationAccount.html +193 -0
- package/docs/html/types/TKeyDerivationChange.html +193 -0
- package/docs/html/types/TKeyDerivationCoinType.html +193 -0
- package/docs/html/types/TKeyDerivationIndex.html +193 -0
- package/docs/html/types/TKeyDerivationPurpose.html +193 -0
- package/docs/html/types/TMessageDataMap.html +210 -0
- package/docs/html/types/TMessageKeys.html +193 -0
- package/docs/html/types/TOnMessage.html +213 -0
- package/docs/html/types/TProcessUnconfirmedTransactions.html +202 -0
- package/docs/html/types/TProtocol.html +193 -0
- package/docs/html/types/TServer.html +204 -0
- package/docs/html/types/TSetData.html +213 -0
- package/docs/html/types/TSetupTransactionResponse.html +193 -0
- package/docs/html/types/TStorage.html +200 -0
- package/docs/html/types/TSubscribedReceive.html +193 -0
- package/docs/html/types/TTransactionMessage.html +198 -0
- package/docs/html/types/TTxResponse.html +206 -0
- package/docs/html/types/TTxResult.html +200 -0
- package/docs/html/types/TUnspentAddressScriptHashData.html +198 -0
- package/docs/html/types/TWalletDataKeys.html +193 -0
- package/docs/html/variables/defaultElectrumPorts.html +193 -0
- package/docs/html/variables/electrumConnection.html +199 -0
- package/docs/html/variables/mostUsedExchangeTickers.html +241 -0
- package/docs/markdown/.nojekyll +1 -0
- package/docs/markdown/README.md +1875 -0
- package/docs/markdown/classes/Electrum.md +718 -0
- package/docs/markdown/classes/Transaction.md +536 -0
- package/docs/markdown/classes/Wallet.md +2189 -0
- package/docs/markdown/enums/EAddressType.md +41 -0
- package/docs/markdown/enums/EAvailableNetworks.md +85 -0
- package/docs/markdown/enums/EBoostType.md +30 -0
- package/docs/markdown/enums/EFeeId.md +63 -0
- package/docs/markdown/enums/EPaymentType.md +30 -0
- package/docs/markdown/enums/EProtocol.md +30 -0
- package/docs/markdown/enums/EUnit.md +41 -0
- package/docs/markdown/interfaces/IAddInput.md +41 -0
- package/docs/markdown/interfaces/IAddress.md +69 -0
- package/docs/markdown/interfaces/IAddressData.md +41 -0
- package/docs/markdown/interfaces/IAddressType.md +7 -0
- package/docs/markdown/interfaces/IAddressTypeData.md +74 -0
- package/docs/markdown/interfaces/IAddresses.md +7 -0
- package/docs/markdown/interfaces/IBoostedTransaction.md +52 -0
- package/docs/markdown/interfaces/IBoostedTransactions.md +7 -0
- package/docs/markdown/interfaces/ICreateTransaction.md +30 -0
- package/docs/markdown/interfaces/ICustomGetAddress.md +41 -0
- package/docs/markdown/interfaces/ICustomGetScriptHash.md +30 -0
- package/docs/markdown/interfaces/IElectrumGetAddressBalanceRes.md +55 -0
- package/docs/markdown/interfaces/IExchangeRates.md +7 -0
- package/docs/markdown/interfaces/IFormattedPeerData.md +63 -0
- package/docs/markdown/interfaces/IFormattedTransaction.md +217 -0
- package/docs/markdown/interfaces/IFormattedTransactions.md +7 -0
- package/docs/markdown/interfaces/IGenerateAddresses.md +74 -0
- package/docs/markdown/interfaces/IGenerateAddressesResponse.md +30 -0
- package/docs/markdown/interfaces/IGetAddress.md +41 -0
- package/docs/markdown/interfaces/IGetAddressBalanceRes.md +36 -0
- package/docs/markdown/interfaces/IGetAddressByPath.md +30 -0
- package/docs/markdown/interfaces/IGetAddressHistoryResponse.md +121 -0
- package/docs/markdown/interfaces/IGetAddressResponse.md +41 -0
- package/docs/markdown/interfaces/IGetAddressScriptHashesHistoryResponse.md +63 -0
- package/docs/markdown/interfaces/IGetDerivationPath.md +100 -0
- package/docs/markdown/interfaces/IGetFeeEstimatesResponse.md +52 -0
- package/docs/markdown/interfaces/IGetHeaderResponse.md +63 -0
- package/docs/markdown/interfaces/IGetNextAvailableAddressResponse.md +52 -0
- package/docs/markdown/interfaces/IGetTransactions.md +63 -0
- package/docs/markdown/interfaces/IGetTransactionsFromInputs.md +63 -0
- package/docs/markdown/interfaces/IGetUtxosResponse.md +30 -0
- package/docs/markdown/interfaces/IHeader.md +41 -0
- package/docs/markdown/interfaces/IIndexes.md +52 -0
- package/docs/markdown/interfaces/IKeyDerivationPath.md +69 -0
- package/docs/markdown/interfaces/IKeyDerivationPathData.md +30 -0
- package/docs/markdown/interfaces/INewBlock.md +30 -0
- package/docs/markdown/interfaces/IOnchainFees.md +63 -0
- package/docs/markdown/interfaces/IOutput.md +41 -0
- package/docs/markdown/interfaces/IPeerData.md +41 -0
- package/docs/markdown/interfaces/IRbfData.md +74 -0
- package/docs/markdown/interfaces/ISend.md +30 -0
- package/docs/markdown/interfaces/ISendTransaction.md +184 -0
- package/docs/markdown/interfaces/ISendTx.md +41 -0
- package/docs/markdown/interfaces/ISetupTransaction.md +52 -0
- package/docs/markdown/interfaces/ISubscribeToAddress.md +60 -0
- package/docs/markdown/interfaces/ISubscribeToHeader.md +59 -0
- package/docs/markdown/interfaces/ITargets.md +52 -0
- package/docs/markdown/interfaces/ITransaction.md +88 -0
- package/docs/markdown/interfaces/ITxHash.md +19 -0
- package/docs/markdown/interfaces/ITxHashes.md +55 -0
- package/docs/markdown/interfaces/IUtxo.md +112 -0
- package/docs/markdown/interfaces/IVin.md +70 -0
- package/docs/markdown/interfaces/IVout.md +52 -0
- package/docs/markdown/interfaces/IWallet.md +209 -0
- package/docs/markdown/interfaces/IWalletData.md +217 -0
- package/example/helpers.ts +76 -0
- package/example/index.ts +77 -0
- package/package.json +69 -0
- package/src/electrum/constants.ts +28 -0
- package/src/electrum/index.ts +838 -0
- package/src/index.ts +6 -0
- package/src/shapes/electrum.ts +13 -0
- package/src/shapes/index.ts +2 -0
- package/src/shapes/wallet.ts +133 -0
- package/src/transaction/index.ts +1078 -0
- package/src/types/electrum.ts +139 -0
- package/src/types/index.ts +3 -0
- package/src/types/transaction.ts +58 -0
- package/src/types/wallet.ts +503 -0
- package/src/utils/conversion.ts +88 -0
- package/src/utils/derivation-path.ts +171 -0
- package/src/utils/electrum.ts +188 -0
- package/src/utils/exchange-rate.ts +70 -0
- package/src/utils/helpers.ts +191 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/result.ts +79 -0
- package/src/utils/transaction.ts +417 -0
- package/src/utils/wallet.ts +351 -0
- package/src/wallet/constants.ts +24 -0
- package/src/wallet/index.ts +3304 -0
- package/tests/constants.ts +9 -0
- package/tests/derivation.test.ts +146 -0
- package/tests/electrum.test.ts +137 -0
- package/tests/storage.test.ts +222 -0
- package/tests/transaction.test.ts +187 -0
- package/tests/wallet.test.ts +307 -0
- package/tsconfig.json +32 -0
- 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
|
+
}
|