From 86a1b2d4f4b980b181c7f1f12da2f09d9ae8cf9d Mon Sep 17 00:00:00 2001 From: Alex Werner Date: Sat, 27 Nov 2021 21:14:11 +0100 Subject: [PATCH] feat: integrate store and keychains --- fixtures/chains/testnet/blockheaders.json | 12 ++ fixtures/chains/testnet/transactions.json | 14 ++ src/EVENTS.js | 1 + src/index.d.ts | 5 + src/index.js | 12 +- src/plugins/Plugins/ChainPlugin.js | 19 ++- src/plugins/Workers/IdentitySyncWorker.js | 3 +- .../TransactionSyncStreamWorker.js | 21 +-- .../handlers/onStreamError.js | 1 + .../methods/getLastSyncedBlockHeight.js | 8 +- .../methods/handleTransactionFromStream.js | 5 +- .../methods/processChunks.js | 15 +- .../methods/setLastSyncedBlockHeight.js | 12 +- src/types/Account/Account.js | 114 ++++++++----- src/types/Account/_initializeAccount.js | 69 ++++---- src/types/Account/methods/disconnect.spec.js | 1 - src/types/Account/methods/generateAddress.js | 50 +++--- src/types/Account/methods/getAddress.js | 47 ++---- src/types/Account/methods/getAddresses.js | 53 ++++-- .../Account/methods/getConfirmedBalance.js | 11 +- ...ce.spec.js => getConfirmedBalance.spec.js} | 3 +- src/types/Account/methods/getTotalBalance.js | 11 +- .../Account/methods/getTotalBalance.spec.js | 43 +++++ src/types/Account/methods/getTransaction.js | 15 +- .../Account/methods/getTransactionHistory.js | 122 +++++++------- src/types/Account/methods/getTransactions.js | 9 +- src/types/Account/methods/getUTXOS.js | 60 +++---- .../Account/methods/getUnconfirmedBalance.js | 10 +- .../methods/getUnconfirmedBalance.spec.js | 43 +++++ src/types/Account/methods/getUnusedAddress.js | 49 ++++-- .../Account/methods/getUnusedAddress.spec.js | 1 - .../Account/methods/importBlockHeader.js | 12 +- .../Account/methods/importTransactions.js | 42 ++--- src/types/Wallet/Wallet.js | 19 +-- src/types/Wallet/methods/fromAddress.js | 4 +- src/types/Wallet/methods/fromMnemonic.js | 8 +- src/types/Wallet/methods/fromPrivateKey.js | 4 +- src/types/Wallet/methods/fromPublicKey.js | 4 +- src/types/Wallet/methods/fromSeed.js | 4 +- src/types/types.d.ts | 8 +- src/utils/bip44/ensureAddressesToGapLimit.js | 1 - src/utils/calculateDuffBalance.js | 62 +++++++ src/utils/index.js | 2 + tests/functional/chainStore.spec.js | 153 ++++++++++++++++++ tests/functional/identitiesStore.spec.js | 24 +++ tests/functional/offlineWallet.spec.js | 83 ++++++++++ tests/functional/storage.spec.js | 35 ++++ tests/functional/walletStore.spec.js | 14 ++ 48 files changed, 929 insertions(+), 389 deletions(-) create mode 100644 fixtures/chains/testnet/blockheaders.json create mode 100644 fixtures/chains/testnet/transactions.json rename src/types/Account/methods/{getBalance.spec.js => getConfirmedBalance.spec.js} (94%) create mode 100644 src/types/Account/methods/getTotalBalance.spec.js create mode 100644 src/types/Account/methods/getUnconfirmedBalance.spec.js create mode 100644 src/utils/calculateDuffBalance.js create mode 100644 tests/functional/chainStore.spec.js create mode 100644 tests/functional/identitiesStore.spec.js create mode 100644 tests/functional/offlineWallet.spec.js create mode 100644 tests/functional/storage.spec.js create mode 100644 tests/functional/walletStore.spec.js diff --git a/fixtures/chains/testnet/blockheaders.json b/fixtures/chains/testnet/blockheaders.json new file mode 100644 index 000000000..d58e0885f --- /dev/null +++ b/fixtures/chains/testnet/blockheaders.json @@ -0,0 +1,12 @@ +[ + { + "height": 600000, + "blockheader": "00000020a69faca490012f4371e4bd40a40167809e6fe1b03c9c5dcb7df247f2fa000000b4b8c2d4a78fb35e50a4eda0bd3a5b9b32f4d739b28a4266f24775b3b79d5b61c73275610e04021ea74d0000", + "hash":"000000de786e659950e0f27681faf1a91871d15de264d0b769cb5941c1d807c3" + }, + { + "height": 610000, + "blockheader": "00000020af5155e1f8d5b60fb247ac9bb8badbdce1fa72302336c7daab53585caf0000006d495b9629989ac3699d528956b1f617fb51a199ef4277c3f45f4606aba8e461d1c78a61210b011e94ef0000", + "hash":"00000094d124cfb68d6d59ffaec9f7d63965cb894855684e23a586274b49708f" + } +] \ No newline at end of file diff --git a/fixtures/chains/testnet/transactions.json b/fixtures/chains/testnet/transactions.json new file mode 100644 index 000000000..5fd376a7a --- /dev/null +++ b/fixtures/chains/testnet/transactions.json @@ -0,0 +1,14 @@ +[ + { + "blockHash": "000000b2537d9b3468e02dc6f49fb8bbb5c9d8b77895df1e76816fbad5555d7d", + "transaction": "02000000019583d226737edfad682cc1f0297f688bfca4c0046062ed3e88fa47dd47eab2b2010000006a473044022021550232cc659fa412deb2ea98956faf9ff0efea3a04029bb63022f6809105ce0220230a3f955a3ee7563f5b006342d8db940f3b29f4e312701bf51720575c7a7a8a012103f3d2b2ddfe6ffad2b8fc1708f2eafadfed36bf31a05dfa04b1fc176526475b0ffeffffff02a003d806000000001976a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac4c5bc92b250000001976a9143e89746b9aa52703ab784bc0df467b160406ffb988acb56b0900", + "height": 617398, + "isInstantLocked": true, + "isChainLocked": true + }, + { + "transaction": "0200000001c7d66bb85e0069c221b44b07f49f52cc4f2e54f70e14430b94888327763a66a9010000006b483045022100da5b319f73e6adfee751f33308f5a8c1fceeab2683e15e132d79053b3118639602204262022fb85f88d9802649a289a1134b678efcf708faaeae8f101e8eab785054012102bc626898b49f31f5194de7bc68004401639a20cfa82e4c2eac9684a91fc47a57feffffff0270f2f605000000001976a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac912e250c250000001976a91415e1edb5c5d9e67d0e36f94343b3eff26bb76d1088ac266e0900", + "blockHash":"0000005c81a683007e86e75c76b4b2feca229f806702ca92953562f2ae628ce7", + "height": 618023 + } +] \ No newline at end of file diff --git a/src/EVENTS.js b/src/EVENTS.js index 7be5a5edd..1f6448ffc 100644 --- a/src/EVENTS.js +++ b/src/EVENTS.js @@ -10,6 +10,7 @@ module.exports = { TRANSACTION: 'transaction', BLOCKHEADER: 'blockheader', FETCHED_ADDRESS: 'FETCHED/ADDRESS', + UPDATED_ADDRESS: 'UPDATED/ADDRESS', ERROR_UPDATE_ADDRESS: 'ERROR/UPDATE_ADDRESS', FETCHED_TRANSACTION: 'FETCHED/TRANSACTION', FETCHED_UNCONFIRMED_TRANSACTION: 'FETCHED/UNCONFIRMED_TRANSACTION', diff --git a/src/index.d.ts b/src/index.d.ts index 7ac3af55a..20ab7dd08 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -4,8 +4,11 @@ import { Account } from "./types/Account/Account"; import { Wallet } from "./types/Wallet/Wallet"; import { Identities } from "./types/Identities/Identities"; +import { IdentitiesStore } from "./types/IdentitiesStore/IdentitiesStore"; import { KeyChain } from "./types/KeyChain/KeyChain"; import { KeyChainStore } from "./types/KeyChainStore/KeyChainStore"; +import { Storage } from "./types/Storage/Storage"; +import { ChainStore } from "./types/ChainStore/ChainStore"; import CONSTANTS from "./CONSTANTS"; import EVENTS from "./EVENTS"; import utils from "./utils"; @@ -14,9 +17,11 @@ import plugins from "./plugins"; export { Account, Wallet, + ChainStore, KeyChain, KeyChainStore, Identities, + IdentitiesStore, EVENTS, CONSTANTS, utils, diff --git a/src/index.js b/src/index.js index a2858bf40..5919e2bdf 100644 --- a/src/index.js +++ b/src/index.js @@ -2,22 +2,30 @@ // polyfill included here. Making it work with webpack is rather tricky, so it is used as per // documentation: https://github.com/YuzuJS/setImmediate#usage require('setimmediate'); -const Wallet = require('./types/Wallet/Wallet'); const Account = require('./types/Account/Account'); +const ChainStore = require('./types/ChainStore/ChainStore'); const Identities = require('./types/Identities/Identities'); +const IdentitiesStore = require('./types/IdentitiesStore/IdentitiesStore'); const KeyChain = require('./types/KeyChain/KeyChain'); const KeyChainStore = require('./types/KeyChainStore/KeyChainStore'); +const Storage = require('./types/Storage/Storage'); +const Wallet = require('./types/Wallet/Wallet'); +const WalletStore = require('./types/WalletStore/WalletStore'); const EVENTS = require('./EVENTS'); const CONSTANTS = require('./CONSTANTS'); const utils = require('./utils'); const plugins = require('./plugins'); module.exports = { - Wallet, Account, + ChainStore, Identities, + IdentitiesStore, KeyChain, KeyChainStore, + Storage, + Wallet, + WalletStore, EVENTS, CONSTANTS, utils, diff --git a/src/plugins/Plugins/ChainPlugin.js b/src/plugins/Plugins/ChainPlugin.js index af0d1c45d..9c659c375 100644 --- a/src/plugins/Plugins/ChainPlugin.js +++ b/src/plugins/Plugins/ChainPlugin.js @@ -33,7 +33,10 @@ class ChainPlugin extends StandardPlugin { */ async execBlockListener() { const self = this; - const { network } = this.storage.store.wallets[this.walletId]; + const { network } = this.storage.application; + const chainStore = this.storage.getChainStore(network); + + // const { network } = this.storage.store.wallets[this.walletId]; if (!this.isSubscribedToBlocks) { self.transport.on(EVENTS.BLOCK, async (ev) => { @@ -41,7 +44,7 @@ class ChainPlugin extends StandardPlugin { const { payload: block } = ev; this.parentEvents.emit(EVENTS.BLOCK, { type: EVENTS.BLOCK, payload: block }); // We do not announce BLOCKHEADER as this is done by Storage - await self.storage.importBlockHeader(block.header); + await chainStore.importBlockHeader(block.header); }); self.transport.on(EVENTS.BLOCKHEIGHT_CHANGED, async (ev) => { const { payload: blockheight } = ev; @@ -50,7 +53,7 @@ class ChainPlugin extends StandardPlugin { type: EVENTS.BLOCKHEIGHT_CHANGED, payload: blockheight, }); - this.storage.store.chains[network.toString()].blockHeight = blockheight; + chainStore.state.blockHeight = blockheight; logger.debug(`ChainPlugin - setting chain blockheight ${blockheight}`); }); await self.transport.subscribeToBlocks(); @@ -69,20 +72,20 @@ class ChainPlugin extends StandardPlugin { return false; } + const { network } = this.storage.application; + const chainStore = this.storage.getChainStore(network); const { chain: { blocksCount: blocks }, network: { fee: { relay } } } = res; - const { network } = this.storage.store.wallets[this.walletId]; - logger.debug('ChainPlugin - Setting up starting blockHeight', blocks); - this.storage.store.chains[network.toString()].blockHeight = blocks; + chainStore.state.blockHeight = blocks; if (relay) { - this.storage.store.chains[network.toString()].fees.minRelay = dashToDuffs(relay); + chainStore.state.fees.minRelay = dashToDuffs(relay); } const bestBlock = await this.transport.getBlockHeaderByHeight(blocks); - await this.storage.importBlockHeader(bestBlock); + await chainStore.importBlockHeader(bestBlock); return true; } diff --git a/src/plugins/Workers/IdentitySyncWorker.js b/src/plugins/Workers/IdentitySyncWorker.js index c11cdd4e4..1e32515bb 100644 --- a/src/plugins/Workers/IdentitySyncWorker.js +++ b/src/plugins/Workers/IdentitySyncWorker.js @@ -26,7 +26,8 @@ class IdentitySyncWorker extends Worker { } async execute() { - const indexedIds = await this.storage.getIndexedIdentityIds(this.walletId); + const walletStore = this.storage.getWalletStore(this.walletId); + const indexedIds = await walletStore.getIndexedIdentityIds(); // Add gaps to empty indices const unusedIndices = []; diff --git a/src/plugins/Workers/TransactionSyncStreamWorker/TransactionSyncStreamWorker.js b/src/plugins/Workers/TransactionSyncStreamWorker/TransactionSyncStreamWorker.js index 0f17d76a1..a322c35eb 100644 --- a/src/plugins/Workers/TransactionSyncStreamWorker/TransactionSyncStreamWorker.js +++ b/src/plugins/Workers/TransactionSyncStreamWorker/TransactionSyncStreamWorker.js @@ -125,7 +125,6 @@ class TransactionSyncStreamWorker extends Worker { .getMessagesList() .map((instantSendLock) => new InstantLock(Buffer.from(instantSendLock))); } - return walletTransactions; } @@ -136,7 +135,7 @@ class TransactionSyncStreamWorker extends Worker { const { skipSynchronizationBeforeHeight, skipSynchronization, - } = (this.storage.store.syncOptions || {}); + } = (this.storage.application.syncOptions || {}); if (skipSynchronization) { logger.debug('TransactionSyncStreamWorker - Wallet created from a new mnemonic. Sync from the best block height.'); @@ -239,25 +238,15 @@ class TransactionSyncStreamWorker extends Worker { } setLastSyncedBlockHash(hash) { - const { walletId } = this; - const accountsStore = this.storage.store.wallets[walletId].accounts; - - const accountStore = ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(this.walletType)) - ? accountsStore[this.BIP44PATH.toString()] - : accountsStore[this.index.toString()]; + const applicationStore = this.storage.application; - accountStore.blockHash = hash; + applicationStore.blockHash = hash; - return accountStore.blockHash; + return applicationStore.blockHash; } getLastSyncedBlockHash() { - const { walletId } = this; - const accountsStore = this.storage.store.wallets[walletId].accounts; - - const { blockHash } = ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(this.walletType)) - ? accountsStore[this.BIP44PATH.toString()] - : accountsStore[this.index.toString()]; + const { blockHash } = this.storage.application; return blockHash; } diff --git a/src/plugins/Workers/TransactionSyncStreamWorker/handlers/onStreamError.js b/src/plugins/Workers/TransactionSyncStreamWorker/handlers/onStreamError.js index fb3577c98..624c58210 100644 --- a/src/plugins/Workers/TransactionSyncStreamWorker/handlers/onStreamError.js +++ b/src/plugins/Workers/TransactionSyncStreamWorker/handlers/onStreamError.js @@ -2,6 +2,7 @@ const logger = require('../../../../logger'); function onStreamError(error, reject) { logger.silly('TransactionSyncStreamWorker - end stream on error'); + logger.silly(error.message); reject(error); } module.exports = onStreamError; diff --git a/src/plugins/Workers/TransactionSyncStreamWorker/methods/getLastSyncedBlockHeight.js b/src/plugins/Workers/TransactionSyncStreamWorker/methods/getLastSyncedBlockHeight.js index ef9ca277d..85e0a4aea 100644 --- a/src/plugins/Workers/TransactionSyncStreamWorker/methods/getLastSyncedBlockHeight.js +++ b/src/plugins/Workers/TransactionSyncStreamWorker/methods/getLastSyncedBlockHeight.js @@ -1,15 +1,9 @@ -const { WALLET_TYPES } = require('../../../../CONSTANTS'); /** * Return last synced block height * @return {number} */ module.exports = function getLastSyncedBlockHeight() { - const { walletId } = this; - const accountsStore = this.storage.store.wallets[walletId].accounts; - - let { blockHeight } = ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(this.walletType)) - ? accountsStore[this.BIP44PATH.toString()] - : accountsStore[this.index.toString()]; + let { blockHeight } = this.storage.application; // Fix Genesis issue on DCore if (blockHeight === 0) blockHeight = 1; diff --git a/src/plugins/Workers/TransactionSyncStreamWorker/methods/handleTransactionFromStream.js b/src/plugins/Workers/TransactionSyncStreamWorker/methods/handleTransactionFromStream.js index 299e4c48c..eca6ee66b 100644 --- a/src/plugins/Workers/TransactionSyncStreamWorker/methods/handleTransactionFromStream.js +++ b/src/plugins/Workers/TransactionSyncStreamWorker/methods/handleTransactionFromStream.js @@ -7,7 +7,6 @@ async function handleTransactionFromStream(transaction) { // eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-underscore-dangle const transactionHash = transaction.hash; - this.pendingRequest[transactionHash] = { isProcessing: true, type: 'transaction' }; // eslint-disable-next-line no-await-in-loop const getTransactionResponse = await this.transport.getTransaction(transactionHash); @@ -62,8 +61,8 @@ async function handleTransactionFromStream(transaction) { const metadata = { blockHash: getTransactionResponse.blockHash, height: getTransactionResponse.height, - instantLocked: getTransactionResponse.instantLocked, - chainLocked: getTransactionResponse.chainLocked, + instantLocked: getTransactionResponse.isInstantLocked, + chainLocked: getTransactionResponse.isChainLocked, }; delete this.pendingRequest[transactionHash]; diff --git a/src/plugins/Workers/TransactionSyncStreamWorker/methods/processChunks.js b/src/plugins/Workers/TransactionSyncStreamWorker/methods/processChunks.js index 19e51bff8..3229db4e7 100644 --- a/src/plugins/Workers/TransactionSyncStreamWorker/methods/processChunks.js +++ b/src/plugins/Workers/TransactionSyncStreamWorker/methods/processChunks.js @@ -27,11 +27,12 @@ async function processChunks(dataChunk) { .filterWalletTransactions(transactionsFromResponse, addresses, network); if (walletTransactions.transactions.length) { + // Normalizing format of transaction for account.importTransactions + const walletTransactionWithoutMetadata = walletTransactions.transactions.map((tx) => [tx]); // When a transaction exist, there is multiple things we need to do : // 1) The transaction itself needs to be imported const addressesGeneratedCount = await self - .importTransactions(walletTransactions.transactions); - + .importTransactions(walletTransactionWithoutMetadata); // 2) Transaction metadata need to be fetched and imported as well. // as such event might happen in the future // As we require height information, we fetch transaction using client @@ -80,10 +81,10 @@ async function processChunks(dataChunk) { // Wrapping `cancel` in `setImmediate` due to bug with double-free // explained here (https://github.com/grpc/grpc-node/issues/1652) // and here (https://github.com/nodejs/node/issues/38964) - await new Promise((resolveCancel) => setImmediate(() => { - self.stream.cancel(); - resolveCancel(); - })); + // await new Promise((resolveCancel) => setImmediate(() => { + // self.stream.cancel(); + // resolveCancel(); + // })); } } } @@ -95,7 +96,7 @@ async function processChunks(dataChunk) { if (merkleBlockFromResponse) { // Reverse hashes, as they're little endian in the header const transactionsInHeader = merkleBlockFromResponse.hashes.map((hashHex) => Buffer.from(hashHex, 'hex').reverse().toString('hex')); - const transactionsInWallet = Object.keys(self.storage.getStore().transactions); + const transactionsInWallet = [...self.storage.getChainStore(self.network).state.transactions.keys()]; const isTruePositive = isAnyIntersection(transactionsInHeader, transactionsInWallet); if (isTruePositive) { self.importBlockHeader(merkleBlockFromResponse.header); diff --git a/src/plugins/Workers/TransactionSyncStreamWorker/methods/setLastSyncedBlockHeight.js b/src/plugins/Workers/TransactionSyncStreamWorker/methods/setLastSyncedBlockHeight.js index 19bd654ac..05579ea35 100644 --- a/src/plugins/Workers/TransactionSyncStreamWorker/methods/setLastSyncedBlockHeight.js +++ b/src/plugins/Workers/TransactionSyncStreamWorker/methods/setLastSyncedBlockHeight.js @@ -7,14 +7,8 @@ const { WALLET_TYPES } = require('../../../../CONSTANTS'); * @return {number} */ module.exports = function setLastSyncedBlockHeight(blockHeight) { - const { walletId } = this; - const accountsStore = this.storage.store.wallets[walletId].accounts; + const applicationStore = this.storage.application; + applicationStore.blockHeight = blockHeight; - const accountStore = ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(this.walletType)) - ? accountsStore[this.BIP44PATH.toString()] - : accountsStore[this.index.toString()]; - - accountStore.blockHeight = blockHeight; - - return accountStore.blockHeight; + return applicationStore.blockHeight; }; diff --git a/src/types/Account/Account.js b/src/types/Account/Account.js index 69b263a41..3c0aaa7bc 100644 --- a/src/types/Account/Account.js +++ b/src/types/Account/Account.js @@ -94,6 +94,7 @@ class Account extends EventEmitter { this.storage = wallet.storage; // Forward all storage event + this.storage.on(EVENTS.UPDATED_ADDRESS, (ev) => this.emit(ev.type, ev)); this.storage.on(EVENTS.CONFIGURED, (ev) => this.emit(ev.type, ev)); this.storage.on(EVENTS.REHYDRATE_STATE_FAILED, (ev) => this.emit(ev.type, ev)); this.storage.on(EVENTS.REHYDRATE_STATE_SUCCESS, (ev) => this.emit(ev.type, ev)); @@ -114,48 +115,75 @@ class Account extends EventEmitter { } switch (this.walletType) { case WALLET_TYPES.HDWALLET: - case WALLET_TYPES.HDPUBLIC: - this.storage.createAccount( - this.walletId, - this.BIP44PATH, - this.network, - this.label, - ); + this.accountPath = getBIP44Path(this.network, this.index); + // this.storage + // .getWalletStore(this.walletId) + // .createPathState(this.BIP44PATH); + // this.storage.createAccount( + // this.walletId, + // this.BIP44PATH, + // this.network, + // this.label, + // ); break; + case WALLET_TYPES.HDPUBLIC: case WALLET_TYPES.PRIVATEKEY: case WALLET_TYPES.PUBLICKEY: case WALLET_TYPES.ADDRESS: case WALLET_TYPES.SINGLE_ADDRESS: - this.storage.createSingleAddress( - this.walletId, - this.network, - this.label, - ); + this.accountPath = 'm/0'; + // this.storage + // .getWalletStore(this.walletId) + // .createPathState(this); + // this.storage.createSingleAddress( + // this.walletId, + // this.network, + // this.label, + // ); break; default: throw new Error(`Invalid wallet type ${this.walletType}`); } + this.storage + .getWalletStore(this.walletId) + .createPathState(this.accountPath); + let keyChainStorePath = this.index; const keyChainStoreOpts = {}; - if ([ - WALLET_TYPES.HDWALLET, - WALLET_TYPES.HDPUBLIC, - WALLET_TYPES.PRIVATEKEY].includes(this.walletType) - ) { - keyChainStorePath = this.BIP44PATH; - keyChainStoreOpts.lookAheadOpts = { - paths: { - 'm/0': BIP44_ADDRESS_GAP, - 'm/1': BIP44_ADDRESS_GAP, - }, - }; + switch (this.walletType) { + case WALLET_TYPES.HDPUBLIC: + keyChainStorePath = this.accountPath; + keyChainStoreOpts.lookAheadOpts = { + paths: { + 'm/0': BIP44_ADDRESS_GAP, + }, + }; + break; + case WALLET_TYPES.HDWALLET: + case WALLET_TYPES.HDPRIVATE: + keyChainStorePath = this.BIP44PATH; + keyChainStoreOpts.lookAheadOpts = { + paths: { + 'm/0': BIP44_ADDRESS_GAP, + 'm/1': BIP44_ADDRESS_GAP, + }, + }; + break; + default: + break; } + this.keyChainStore = wallet .keyChainStore .makeChildKeyChainStore(keyChainStorePath, keyChainStoreOpts); + // This forces keychainStore to set to issued key what is already its masterkey + if ([WALLET_TYPES.PUBLICKEY, WALLET_TYPES.PRIVATEKEY].includes(this.walletType)) { + this.keyChainStore.getMasterKeyChain().getForPath('0'); + } + this.cacheTx = (opts.cacheTx) ? opts.cacheTx : defaultOptions.cacheTx; this.cacheBlockHeaders = (opts.cacheBlockHeaders) ? opts.cacheBlockHeaders @@ -167,25 +195,25 @@ class Account extends EventEmitter { watchers: {}, }; - // Handle import of cache - if (opts.cache) { - if (opts.cache.addresses) { - try { - this.storage.importAddresses(opts.cache.addresses, this.walletId); - } catch (e) { - this.disconnect(); - throw e; - } - } - if (opts.cache.transactions) { - try { - this.storage.importTransactions(opts.cache.transactions); - } catch (e) { - this.disconnect(); - throw e; - } - } - } + // // Handle import of cache + // if (opts.cache) { + // if (opts.cache.addresses) { + // try { + // this.storage.importAddresses(opts.cache.addresses, this.walletId); + // } catch (e) { + // this.disconnect(); + // throw e; + // } + // } + // if (opts.cache.transactions) { + // try { + // this.storage.importTransactions(opts.cache.transactions); + // } catch (e) { + // this.disconnect(); + // throw e; + // } + // } + // } this.emit(EVENTS.CREATED, { type: EVENTS.CREATED, payload: null }); } diff --git a/src/types/Account/_initializeAccount.js b/src/types/Account/_initializeAccount.js index 858e74883..67dae65e1 100644 --- a/src/types/Account/_initializeAccount.js +++ b/src/types/Account/_initializeAccount.js @@ -3,10 +3,35 @@ const EVENTS = require('../../EVENTS'); const { WALLET_TYPES } = require('../../CONSTANTS'); const preparePlugins = require('./_preparePlugins'); const ensureAddressesToGapLimit = require('../../utils/bip44/ensureAddressesToGapLimit'); +const { UPDATED_ADDRESS } = require('../../EVENTS'); +const { is } = require('../../utils'); // eslint-disable-next-line no-underscore-dangle async function _initializeAccount(account, userUnsafePlugins) { const self = account; + + function markAddressAsUsed(props) { + const { address } = props.payload; + // This works if the TX cames from our main address, but not in all cases... + self.keyChainStore + .getMasterKeyChain() + .markAddressAsUsed(address); + } + + self.on(UPDATED_ADDRESS, markAddressAsUsed); + + const accountStore = account.storage + .getWalletStore(account.walletId) + .getPathState(account.accountPath); + + const chainStore = account.storage.getChainStore(account.network); + + const issuedPaths = account.keyChainStore.getMasterKeyChain().getIssuedPaths(); + issuedPaths.forEach((issuedPath) => { + accountStore.addresses[issuedPath.path] = issuedPath.address.toString(); + chainStore.importAddress(issuedPath.address.toString()); + }); + // We run faster in offlineMode to speed up the process when less happens. const readinessIntervalTime = (account.offlineMode) ? 50 : 200; // TODO: perform rejection with a timeout @@ -14,16 +39,16 @@ async function _initializeAccount(account, userUnsafePlugins) { return new Promise(async (resolve, reject) => { try { if (account.injectDefaultPlugins) { - if ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(account.walletType)) { - ensureAddressesToGapLimit( - account.store.wallets[account.walletId], - account.walletType, - account.index, - account.getAddress.bind(account), - ); - } else { - await account.getAddress('0'); // We force what is usually done by the BIP44Worker. - } + // if ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(account.walletType)) { + // ensureAddressesToGapLimit( + // account.store.wallets[account.walletId], + // account.walletType, + // account.index, + // account.getAddress.bind(account), + // ); + // } else { + // await account.getAddress('0'); // We force what is usually done by the BIP44Worker. + // } } // Will sort and inject plugins. @@ -68,35 +93,11 @@ async function _initializeAccount(account, userUnsafePlugins) { // while SyncWorker fetch'em on network clearInterval(self.readinessInterval); - switch (account.walletType) { - case WALLET_TYPES.PRIVATEKEY: - case WALLET_TYPES.SINGLE_ADDRESS: - account.generateAddress(0); - sendReady(); - return resolve(true); - case WALLET_TYPES.PUBLICKEY: - case WALLET_TYPES.ADDRESS: - account.generateAddress(0); - sendReady(); - return resolve(true); - default: - break; - } - if (!account.injectDefaultPlugins) { sendReady(); return resolve(true); } - if ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(account.walletType)) { - ensureAddressesToGapLimit( - account.store.wallets[account.walletId], - account.walletType, - account.index, - account.getAddress.bind(account), - ); - } - sendReady(); return resolve(true); } diff --git a/src/types/Account/methods/disconnect.spec.js b/src/types/Account/methods/disconnect.spec.js index 0e2036fb3..aa0fcfe3c 100644 --- a/src/types/Account/methods/disconnect.spec.js +++ b/src/types/Account/methods/disconnect.spec.js @@ -33,7 +33,6 @@ describe('Account - disconnect', function suite() { it('should disconnect to stream and worker', async () => { expect(transportConnected).to.equal(true); await disconnect.call(self); - // console.log(self, transportConnected, emitted); expect(transportConnected).to.equal(false); expect(emitted).to.deep.equal([ 'WORKER/DUMMYWORKER/STARTING', diff --git a/src/types/Account/methods/generateAddress.js b/src/types/Account/methods/generateAddress.js index 19f71f6be..c24169b00 100644 --- a/src/types/Account/methods/generateAddress.js +++ b/src/types/Account/methods/generateAddress.js @@ -6,14 +6,13 @@ const { is } = require('../../../utils'); * Generate an address from a path and import it to the store * @param {string} path * @param {boolean} [isWatchedAddress=true] - if the address will be watched - * @return {AddressObj} Address information + * @return {AddressInfo} Address information * */ function generateAddress(path, isWatchedAddress = true) { if (is.undefOrNull(path)) throw new Error('Expected path to generate an address'); let index = 0; - let privateKey; let address; - let keyData; + let keyPathData; const { network } = this; switch (this.walletType) { @@ -46,59 +45,48 @@ function generateAddress(path, isWatchedAddress = true) { case WALLET_TYPES.HDWALLET: // eslint-disable-next-line prefer-destructuring index = parseInt(path.toString().split('/')[2], 10); - keyData = this.keyChainStore + keyPathData = this.keyChainStore .getMasterKeyChain() .getForPath(path, { isWatched: isWatchedAddress }); - address = keyData.address.toString(); - privateKey = keyData.key; + address = keyPathData.address.toString(); break; case WALLET_TYPES.HDPUBLIC: index = parseInt(path.toString().split('/')[5], 10); // eslint-disable-next-line no-case-declarations - keyData = this.keyChainStore + keyPathData = this.keyChainStore .getMasterKeyChain() .getForPath(path, { isWatched: isWatchedAddress }); - privateKey = keyData.key; - address = keyData.address.toString(); + address = keyPathData.address.toString(); break; // TODO: DEPRECATE USAGE OF SINGLE_ADDRESS in favor or PRIVATEKEY case WALLET_TYPES.PRIVATEKEY: case WALLET_TYPES.SINGLE_ADDRESS: - privateKey = this.keyChainStore.getMasterKeyChain().rootKey.toString(); - address = privateKey.publicKey.toAddress(network).toString(); - - if (isWatchedAddress) { - this.keyChainStore - .issuedPaths.set(0, { - key: privateKey, - path: 0, - address, - isUsed: false, - isWatched: true, - }); - } - break; default: - privateKey = this.keyChainStore + keyPathData = this.keyChainStore .getMasterKeyChain() - .getKeyForPath(path.toString(), { isWatched: isWatchedAddress }).key; - address = privateKey.publicKey.toAddress(network).toString(); + .getForPath(path, { isWatched: isWatchedAddress }); + address = keyPathData.address.toString(); + break; } const addressData = { path: path.toString(), index, address, - // privateKey, transactions: [], + utxos: {}, balanceSat: 0, unconfirmedBalanceSat: 0, - utxos: {}, - fetchedLast: 0, - used: false, }; - this.storage.importAddresses(addressData, this.walletId); + const accountStore = this.storage + .getWalletStore(this.walletId) + .getPathState(this.accountPath); + + const chainStore = this.storage.getChainStore(this.network); + + accountStore.addresses[addressData.path] = addressData.address.toString(); + chainStore.importAddress(addressData.address.toString()); this.emit(EVENTS.GENERATED_ADDRESS, { type: EVENTS.GENERATED_ADDRESS, payload: addressData }); return addressData; } diff --git a/src/types/Account/methods/getAddress.js b/src/types/Account/methods/getAddress.js index ed89c24ff..86efad073 100644 --- a/src/types/Account/methods/getAddress.js +++ b/src/types/Account/methods/getAddress.js @@ -1,41 +1,26 @@ const { WALLET_TYPES } = require('../../../CONSTANTS'); -const getTypePathFromWalletType = (walletType, addressType = 'external', accountIndex, addressIndex) => { - let type; - let path; - - const addressTypeIndex = (addressType === 'external') ? 0 : 1; - switch (walletType) { - case WALLET_TYPES.HDWALLET: - type = addressType; - path = `m/${addressTypeIndex}/${addressIndex}`; - break; - case WALLET_TYPES.HDPUBLIC: - type = 'external'; - path = `m/${addressTypeIndex}/${addressIndex}`; - break; - case WALLET_TYPES.PUBLICKEY: - case WALLET_TYPES.ADDRESS: - case WALLET_TYPES.PRIVATEKEY: - case WALLET_TYPES.SINGLE_ADDRESS: - default: - type = 'misc'; - path = '0'; - } - return { type, path }; -}; /** * Get a specific addresss based on the index and type of address. * @param {number} index - The index on the type - * @param {AddressType} [_type="external"] - Type of the address (external, internal, misc) + * @param {AddressType} [addressType="external"] - Type of the address (external, internal, misc) * @return */ -function getAddress(addressIndex = 0, _type = 'external') { - const accountIndex = this.index; - const { type, path } = getTypePathFromWalletType(this.walletType, _type, accountIndex, addressIndex); +function getAddress(addressIndex = 0, addressType = 'external') { + const addressTypeIndex = (addressType === 'external') ? 0 : 1; + + const { addresses } = this.storage.getWalletStore(this.walletId).getPathState(this.accountPath); + const addressPath = ([WALLET_TYPES.HDPUBLIC, WALLET_TYPES.HDWALLET].includes(this.walletType)) + ? `m/${addressTypeIndex}/${addressIndex}` : '0'; + + const address = addresses[addressPath]; + if (!address) return this.generateAddress(addressPath); - const { wallets } = this.storage.getStore(); - const matchingTypeAddresses = wallets[this.walletId].addresses[type]; - return (matchingTypeAddresses[path]) ? matchingTypeAddresses[path] : this.generateAddress(path); + const chainStore = this.storage.getChainStore(this.network); + return { + index: addressIndex, + path: addressPath, + ...chainStore.getAddress(address), + }; } module.exports = getAddress; diff --git a/src/types/Account/methods/getAddresses.js b/src/types/Account/methods/getAddresses.js index 92a64e653..8ea2d9c58 100644 --- a/src/types/Account/methods/getAddresses.js +++ b/src/types/Account/methods/getAddresses.js @@ -2,20 +2,47 @@ const { WALLET_TYPES } = require('../../../CONSTANTS'); /** * Get all the addresses from the store from a given type - * @param {AddressType} [_type="external"] - Type of the address (external, internal, misc) + * @param {AddressType} [addressType="external"] - Type of the address (external, internal, misc) * @return {[AddressObj]} address - All address matching the type */ -function getAddresses(_type = 'external') { - const miscTypes = [ - WALLET_TYPES.SINGLE_ADDRESS, - WALLET_TYPES.PUBLICKEY, - WALLET_TYPES.PRIVATEKEY, - WALLET_TYPES.ADDRESS, - ]; - const walletType = (miscTypes.includes(this.walletType)) - ? 'misc' - : ((_type) || 'external'); - const store = this.storage.getStore(); - return store.wallets[this.walletId].addresses[walletType]; +function getAddresses(addressType = 'external') { + // const miscTypes = [ + // WALLET_TYPES.SINGLE_ADDRESS, + // WALLET_TYPES.PUBLICKEY, + // WALLET_TYPES.PRIVATEKEY, + // WALLET_TYPES.ADDRESS, + // ]; + // const walletType = (miscTypes.includes(this.walletType)) + // ? 'misc' + // : ((_type) || 'external'); + // const store = this.storage.getStore(); + // return store.wallets[this.walletId].addresses[walletType]; + const addressTypeIndex = (addressType === 'external') ? 0 : 1; + + const { addresses } = this.storage + .getWalletStore(this.walletId) + .getPathState(this.accountPath); + + const chainStore = this.storage.getChainStore(this.network); + + const baseAddressPath = ([WALLET_TYPES.HDPUBLIC, WALLET_TYPES.HDWALLET].includes(this.walletType)) + ? `m/${addressTypeIndex}` : '0'; + + const typedAddresses = {}; + + Object + .entries(addresses) + .forEach(([path, address]) => { + if (path.startsWith(baseAddressPath)) { + const index = parseInt(path.split('/').slice(-1)[0], 10); + typedAddresses[path] = { + index, + path, + ...chainStore.getAddress(address), + }; + } + }); + + return typedAddresses; } module.exports = getAddresses; diff --git a/src/types/Account/methods/getConfirmedBalance.js b/src/types/Account/methods/getConfirmedBalance.js index c84a71c48..158ca50f0 100644 --- a/src/types/Account/methods/getConfirmedBalance.js +++ b/src/types/Account/methods/getConfirmedBalance.js @@ -1,4 +1,4 @@ -const { duffsToDash } = require('../../../utils'); +const { duffsToDash, calculateDuffBalance } = require('../../../utils'); /** * Return the confirmed balance of an account. @@ -7,10 +7,13 @@ const { duffsToDash } = require('../../../utils'); */ function getConfirmedBalance(displayDuffs = true) { const { - walletId, storage, + walletId, storage, accountPath, network, } = this; - const accountIndex = this.index; - const totalSat = storage.calculateDuffBalance(walletId, accountIndex, 'confirmed'); + + const { addresses } = storage.getWalletStore(walletId).getPathState(accountPath); + + const chainStore = storage.getChainStore(network); + const totalSat = (calculateDuffBalance(Object.values(addresses), chainStore, 'confirmed')); return (displayDuffs) ? totalSat : duffsToDash(totalSat); } diff --git a/src/types/Account/methods/getBalance.spec.js b/src/types/Account/methods/getConfirmedBalance.spec.js similarity index 94% rename from src/types/Account/methods/getBalance.spec.js rename to src/types/Account/methods/getConfirmedBalance.spec.js index 9ade6080a..c5db82094 100644 --- a/src/types/Account/methods/getBalance.spec.js +++ b/src/types/Account/methods/getConfirmedBalance.spec.js @@ -3,7 +3,8 @@ const mockedStore = require('../../../../fixtures/sirentonight-fullstore-snapsho const getTotalBalance = require('./getTotalBalance'); const getConfirmedBalance = require('./getConfirmedBalance'); const getUnconfirmedBalance = require('./getUnconfirmedBalance'); -const calculateDuffBalance = require('../../Storage/methods/calculateDuffBalance'); +const { duffsToDash, calculateDuffBalance } = require('../../../utils'); + let mockedWallet; describe('Account - getTotalBalance', function suite() { diff --git a/src/types/Account/methods/getTotalBalance.js b/src/types/Account/methods/getTotalBalance.js index 8b490ac8f..9c0161549 100644 --- a/src/types/Account/methods/getTotalBalance.js +++ b/src/types/Account/methods/getTotalBalance.js @@ -1,4 +1,4 @@ -const { duffsToDash } = require('../../../utils'); +const { duffsToDash, calculateDuffBalance } = require('../../../utils'); /** * Return the total balance of an account (confirmed + unconfirmed). @@ -7,9 +7,14 @@ const { duffsToDash } = require('../../../utils'); */ function getTotalBalance(displayDuffs = true) { const { - walletId, storage, index, + walletId, storage, accountPath, network, } = this; - const totalSat = storage.calculateDuffBalance(walletId, index, 'total'); + + const { addresses } = storage.getWalletStore(walletId).getPathState(accountPath); + + const chainStore = storage.getChainStore(network); + + const totalSat = (calculateDuffBalance(Object.values(addresses), chainStore, 'total')); return (displayDuffs) ? totalSat : duffsToDash(totalSat); } diff --git a/src/types/Account/methods/getTotalBalance.spec.js b/src/types/Account/methods/getTotalBalance.spec.js new file mode 100644 index 000000000..c5db82094 --- /dev/null +++ b/src/types/Account/methods/getTotalBalance.spec.js @@ -0,0 +1,43 @@ +const { expect } = require('chai'); +const mockedStore = require('../../../../fixtures/sirentonight-fullstore-snapshot-1562711703'); +const getTotalBalance = require('./getTotalBalance'); +const getConfirmedBalance = require('./getConfirmedBalance'); +const getUnconfirmedBalance = require('./getUnconfirmedBalance'); +const { duffsToDash, calculateDuffBalance } = require('../../../utils'); + + +let mockedWallet; +describe('Account - getTotalBalance', function suite() { + this.timeout(10000); + before(() => { + const storageHDW = { + store: mockedStore, + calculateDuffBalance, + getStore: () => mockedStore, + mappedAddress: {}, + }; + const walletId = Object.keys(mockedStore.wallets)[0]; + mockedWallet = { + walletId, + index: 0, + storage: storageHDW, + }; + }); + it('should correctly get the balance', async () => { + const balance = await getTotalBalance.call(mockedWallet); + expect(balance).to.equal(184499999506); + }); + it('should correctly get the balance confirmed only', async () => { + const balance = await getConfirmedBalance.call(mockedWallet); + expect(balance).to.equal(184499999506); + }); + it('should correctly get the balance dash value instead of duff', async () => { + const balanceTotalDash = await getTotalBalance.call(mockedWallet, false); + const balanceUnconfDash = await getUnconfirmedBalance.call(mockedWallet, false); + const balanceConfDash = await getConfirmedBalance.call(mockedWallet, false); + + expect(balanceTotalDash).to.equal(1844.99999506); + expect(balanceUnconfDash).to.equal(0); + expect(balanceConfDash).to.equal(1844.99999506); + }); +}); diff --git a/src/types/Account/methods/getTransaction.js b/src/types/Account/methods/getTransaction.js index 4c7dce06a..da7a09705 100644 --- a/src/types/Account/methods/getTransaction.js +++ b/src/types/Account/methods/getTransaction.js @@ -6,15 +6,14 @@ const EVENTS = require('../../../EVENTS'); * @return {Promise<{metadata: TransactionMetaData|null, transaction: Transaction}>} */ async function getTransaction(txid = null) { - const searchTransaction = await this.storage.searchTransaction(txid); - const searchTransactionMetadata = await this.storage.searchTransactionMetadata(txid); - if (searchTransaction.found) { - const searchResult = { transaction: searchTransaction.result, metadata: null }; - if (searchTransactionMetadata.found) { - searchResult.metadata = searchTransactionMetadata.result; - } - return searchResult; + const { storage, network } = this; + const chainStore = storage.getChainStore(network); + const searchedTransaction = chainStore.getTransaction(txid); + + if (searchedTransaction) { + return searchedTransaction; } + const getTransactionResponse = await this.transport.getTransaction(txid); if (!getTransactionResponse) return null; const { diff --git a/src/types/Account/methods/getTransactionHistory.js b/src/types/Account/methods/getTransactionHistory.js index 3118f363a..e0d132544 100644 --- a/src/types/Account/methods/getTransactionHistory.js +++ b/src/types/Account/methods/getTransactionHistory.js @@ -25,73 +25,77 @@ function getTransactionHistory() { } = this; const transactions = this.getTransactions(); - const store = storage.getStore(); - const chainStore = store.chains[network.toString()]; + const chainStore = storage.getChainStore(network); const { blockHeaders } = chainStore; - const { wallets: walletStore, transactionsMetadata } = store; + // const { wallets: walletStore, transactionsMetadata } = store; - const accountStore = walletStore[walletId]; + // const accountStore = walletStore[walletId]; // In store, not all transaction are specific to this account, we filter our transactions. - const filteredTransactions = filterTransactions( - accountStore, - walletType, - accountIndex, - transactions, - ); - const filteredTransactionsWithMetadata = extendTransactionsWithMetadata( - filteredTransactions, - transactionsMetadata, - ); - - const categorizedTransactions = categorizeTransactions( - filteredTransactionsWithMetadata, - accountStore, - accountIndex, - walletType, - network, - ); - - const sortedCategorizedTransactions = categorizedTransactions.sort(sortByHeightDescending); - - each(sortedCategorizedTransactions, (categorizedTransaction) => { - const { - transaction, - from, - to, - type, - isChainLocked, - isInstantLocked, - } = categorizedTransaction; - - const blockHash = categorizedTransaction.blockHash !== '' ? categorizedTransaction.blockHash : null; - - // To get time of block, let's find the blockheader. - const blockHeader = blockHeaders[blockHash]; - - // If it's unconfirmed, we won't have a blockHeader nor it's time. - const time = blockHeader ? blockHeader.time : -1; - - const normalizedTransactionHistory = { - // Would require knowing the vout of this vin to determinate inputAmount. - // This information could be fetched, but the necessity vs the cost is questionable. - // fees: calculateTransactionFees(categorizedTransaction.transaction), - from, - to, - type, - time, - txId: transaction.hash, - blockHash, - isChainLocked, - isInstantLocked, - }; - - transactionHistory.push(normalizedTransactionHistory); + // const filteredTransactions = filterTransactions( + // accountStore, + // walletType, + // accountIndex, + // transactions, + // ); + // const filteredTransactionsWithMetadata = extendTransactionsWithMetadata( + // filteredTransactions, + // transactionsMetadata, + // ); + + const transactionsWithMetadata = []; + transactions.forEach((transactionId) => { + console.log(chainStore.getTransaction(transactionId)); }); + + // const categorizedTransactions = categorizeTransactions( + // transactionsWithMetadata, + // accountStore, + // accountIndex, + // walletType, + // network, + // ); + // + // const sortedCategorizedTransactions = categorizedTransactions.sort(sortByHeightDescending); + // + // each(sortedCategorizedTransactions, (categorizedTransaction) => { + // const { + // transaction, + // from, + // to, + // type, + // isChainLocked, + // isInstantLocked, + // } = categorizedTransaction; + // + // const blockHash = categorizedTransaction.blockHash !== '' ? categorizedTransaction.blockHash : null; + // + // To get time of block, let's find the blockheader. + // const blockHeader = blockHeaders[blockHash]; + // + // If it's unconfirmed, we won't have a blockHeader nor it's time. + // const time = blockHeader ? blockHeader.time : -1; + + // const normalizedTransactionHistory = { + // Would require knowing the vout of this vin to determinate inputAmount. + // This information could be fetched, but the necessity vs the cost is questionable. + // fees: calculateTransactionFees(categorizedTransaction.transaction), + // from, + // to, + // type, + // time, + // txId: transaction.hash, + // blockHash, + // isChainLocked, + // isInstantLocked, + // }; + + // transactionHistory.push(normalizedTransactionHistory); + // }); // Sort by decreasing time. - return transactionHistory.sort(sortbyTimeDescending); + // return transactionHistory.sort(sortbyTimeDescending); } module.exports = getTransactionHistory; diff --git a/src/types/Account/methods/getTransactions.js b/src/types/Account/methods/getTransactions.js index 453fe3884..59267b006 100644 --- a/src/types/Account/methods/getTransactions.js +++ b/src/types/Account/methods/getTransactions.js @@ -3,6 +3,11 @@ * @return {[Transaction]} transactions - All transaction in the store */ module.exports = function getTransactions() { - const store = this.storage.getStore(); - return store.transactions; + const chainStore = this.storage.getChainStore(this.network); + const transactions = []; + const { addresses } = this.storage.getWalletStore(this.walletId).getPathState(this.accountPath); + Object.values(addresses).forEach((address) => { + transactions.push(...chainStore.getAddress(address).transactions); + }); + return transactions; }; diff --git a/src/types/Account/methods/getUTXOS.js b/src/types/Account/methods/getUTXOS.js index d0dcfcd6b..eab227c02 100644 --- a/src/types/Account/methods/getUTXOS.js +++ b/src/types/Account/methods/getUTXOS.js @@ -11,44 +11,32 @@ const { WALLET_TYPES, COINBASE_MATURITY } = require('../../../CONSTANTS'); function getUTXOS(options = { coinbaseMaturity: COINBASE_MATURITY, }) { - const self = this; const { walletId, network, - BIP44PATH, - walletType, } = this; const utxos = []; - const isHDWallet = [WALLET_TYPES.HDPUBLIC, WALLET_TYPES.HDWALLET].includes(walletType); - const currentBlockHeight = this.store.chains[network].blockHeight; + const chainStore = this.storage.getChainStore(network); + const accountState = this.storage.getWalletStore(walletId).getPathState(this.accountPath); + const currentBlockHeight = chainStore.blockHeight; - for (const addressType in this.store.wallets[walletId].addresses) { - if (!addressType || !['external', 'internal', 'misc'].includes(addressType)) { - continue; - } - for (const path in self.store.wallets[walletId].addresses[addressType]) { - if (!path) continue; - const address = self.store.wallets[walletId].addresses[addressType][path]; - - if (isHDWallet && !path.startsWith(BIP44PATH)) continue; - - for (const identifier in address.utxos) { - if (!identifier) continue; - const [txid, outputIndex] = identifier.split('-'); - const transaction = new Transaction(this.store.transactions[txid]); - - if (transaction.isCoinbase()) { - // If the transaction is not a special transaction, we can't check its - // maturity at the moment of writing this comment. - // The wallet library doesn't maintain the header chain and thus we can - // figure out the height only from the payload, but old coinbase transactions - // doesn't have a payload. - if (!transaction.isSpecialTransaction()) { - continue; - } + Object.values(accountState.addresses).forEach((address) => { + const addressData = chainStore.getAddress(address); + const utxosKeys = Object.keys(addressData.utxos); + utxosKeys.forEach((utxoIdentifier) => { + let skipUtxo = false; + const [txid, outputIndex] = utxoIdentifier.split('-'); + const { transaction } = chainStore.getTransaction(txid); + if (transaction.isCoinbase()) { + // If the transaction is not a special transaction, we can't check its + // maturity at the moment of writing this comment. + // The wallet library doesn't maintain the header chain and thus we can + // figure out the height only from the payload, but old coinbase transactions + // doesn't have a payload. + if (transaction.isSpecialTransaction()) { const transactionHeight = (this.store.transactionsMetadata[txid]) ? this.store.transactionsMetadata[txid].height : transaction.extraPayload.height; @@ -56,22 +44,24 @@ function getUTXOS(options = { // We check maturity is at least 100 blocks. // another way is to just read _scriptBuffer height value. if (transactionHeight + options.coinbaseMaturity > currentBlockHeight) { - continue; + skipUtxo = true; } } + } + if (!skipUtxo) { utxos.push(new Transaction.UnspentOutput( { txId: txid, vout: parseInt(outputIndex, 10), - script: address.utxos[identifier].script, - satoshis: address.utxos[identifier].satoshis, - address: new Address(address.address, network), + script: addressData.utxos[utxoIdentifier].script, + satoshis: addressData.utxos[utxoIdentifier].satoshis, + address: new Address(addressData.address, network), }, )); } - } - } + }); + }); return utxos.sort((a, b) => b.satoshis - a.satoshis); } diff --git a/src/types/Account/methods/getUnconfirmedBalance.js b/src/types/Account/methods/getUnconfirmedBalance.js index 6b320a0ff..9833525b4 100644 --- a/src/types/Account/methods/getUnconfirmedBalance.js +++ b/src/types/Account/methods/getUnconfirmedBalance.js @@ -1,4 +1,4 @@ -const { duffsToDash } = require('../../../utils'); +const { duffsToDash, calculateDuffBalance } = require('../../../utils'); /** * Return the total balance of unconfirmed utxo @@ -7,11 +7,13 @@ const { duffsToDash } = require('../../../utils'); */ function getUnconfirmedBalance(displayDuffs = true) { const { - walletId, storage, + walletId, storage, accountPath, network, } = this; - const accountIndex = this.index; - const totalSat = storage.calculateDuffBalance(walletId, accountIndex, 'unconfirmed'); + const { addresses } = storage.getWalletStore(walletId).getPathState(accountPath); + + const chainStore = storage.getChainStore(network); + const totalSat = (calculateDuffBalance(Object.values(addresses), chainStore, 'unconfirmed')); return (displayDuffs) ? totalSat : duffsToDash(totalSat); } diff --git a/src/types/Account/methods/getUnconfirmedBalance.spec.js b/src/types/Account/methods/getUnconfirmedBalance.spec.js new file mode 100644 index 000000000..c5db82094 --- /dev/null +++ b/src/types/Account/methods/getUnconfirmedBalance.spec.js @@ -0,0 +1,43 @@ +const { expect } = require('chai'); +const mockedStore = require('../../../../fixtures/sirentonight-fullstore-snapshot-1562711703'); +const getTotalBalance = require('./getTotalBalance'); +const getConfirmedBalance = require('./getConfirmedBalance'); +const getUnconfirmedBalance = require('./getUnconfirmedBalance'); +const { duffsToDash, calculateDuffBalance } = require('../../../utils'); + + +let mockedWallet; +describe('Account - getTotalBalance', function suite() { + this.timeout(10000); + before(() => { + const storageHDW = { + store: mockedStore, + calculateDuffBalance, + getStore: () => mockedStore, + mappedAddress: {}, + }; + const walletId = Object.keys(mockedStore.wallets)[0]; + mockedWallet = { + walletId, + index: 0, + storage: storageHDW, + }; + }); + it('should correctly get the balance', async () => { + const balance = await getTotalBalance.call(mockedWallet); + expect(balance).to.equal(184499999506); + }); + it('should correctly get the balance confirmed only', async () => { + const balance = await getConfirmedBalance.call(mockedWallet); + expect(balance).to.equal(184499999506); + }); + it('should correctly get the balance dash value instead of duff', async () => { + const balanceTotalDash = await getTotalBalance.call(mockedWallet, false); + const balanceUnconfDash = await getUnconfirmedBalance.call(mockedWallet, false); + const balanceConfDash = await getConfirmedBalance.call(mockedWallet, false); + + expect(balanceTotalDash).to.equal(1844.99999506); + expect(balanceUnconfDash).to.equal(0); + expect(balanceConfDash).to.equal(1844.99999506); + }); +}); diff --git a/src/types/Account/methods/getUnusedAddress.js b/src/types/Account/methods/getUnusedAddress.js index cdd6334c3..9a5447199 100644 --- a/src/types/Account/methods/getUnusedAddress.js +++ b/src/types/Account/methods/getUnusedAddress.js @@ -10,27 +10,44 @@ function getUnusedAddress(type = 'external', skip = 0) { let unused = { address: '', }; - let skipped = 0; + const skipped = 0; const { walletId } = this; const accountIndex = this.index; - const keys = Object.keys(this.store.wallets[walletId].addresses[type]) - // We filter out other potential account - .filter((el) => parseInt(el.split('/')[3], 10) === accountIndex); - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - const el = (this.store.wallets[walletId].addresses[type][key]); + const { addresses } = this.storage.getWalletStore(walletId).getPathState(this.accountPath); - if (!el || !el.address || el.address === '') { - logger.warn('getUnusedAddress received an empty one.', el, i, skipped); - } - unused = el; - if (el.used === false) { - if (skipped < skip) { - skipped += 1; - } else { - break; + const chainStore = this.storage.getChainStore(this.network); + + // We sort by type + const sortedAddresses = { + external: {}, + internal: {}, + }; + Object + .keys(addresses) + .forEach((path) => { + const splittedPath = path.split('/'); + let pathType = 'external'; + if (splittedPath.length > 1) { + pathType = (splittedPath[splittedPath.length - 2] === '0') ? 'external' : 'internal'; } + sortedAddresses[pathType][path] = addresses[path]; + }); + + const keys = Object.keys(sortedAddresses[type]); + + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + const address = (sortedAddresses[type][key]); + const addressState = chainStore.getAddress(address); + if (!addressState || addressState.transactions.length === 0) { + const keychainData = this.keyChainStore.getMasterKeyChain().getForPath(key); + unused = { + address: keychainData.address.toString(), + path: key, + index: parseInt(key.split('/').splice(-1)[0], 10), + }; + break; } } diff --git a/src/types/Account/methods/getUnusedAddress.spec.js b/src/types/Account/methods/getUnusedAddress.spec.js index 91d0f4400..bc6dc99cc 100644 --- a/src/types/Account/methods/getUnusedAddress.spec.js +++ b/src/types/Account/methods/getUnusedAddress.spec.js @@ -33,7 +33,6 @@ describe('Account - getUnusedAddress', function suite() { const unusedAddressExternal = getUnusedAddress.call(self); const unusedAddressInternal = getUnusedAddress.call(self, 'internal'); - // console.log(mockedStore.wallets[self.walletId].addresses.internal) expect(unusedAddressExternal).to.be.deep.equal({ address: 'yaVrJ5dgELFkYwv6AydDyGPAJQ5kTJXyAN', balanceSat: 0, diff --git a/src/types/Account/methods/importBlockHeader.js b/src/types/Account/methods/importBlockHeader.js index 01e4517a4..79c38b4fd 100644 --- a/src/types/Account/methods/importBlockHeader.js +++ b/src/types/Account/methods/importBlockHeader.js @@ -15,16 +15,14 @@ module.exports = async function importBlockHeader(blockHeader) { // knowing the following blockHeight blockheader's prevHash value // const previousHash = blockHeader.prevHash.reverse().toString('hex'); const { - walletId, BIP44PATH, index, store, storage, walletType, + walletId, BIP44PATH, index, store, storage, network, walletType, } = this; - const localWalletStore = store.wallets[walletId]; - const localAccountStore = ([WALLET_TYPES.HDPUBLIC, WALLET_TYPES.HDWALLET].includes(walletType)) - ? localWalletStore.accounts[BIP44PATH.toString()] - : localWalletStore.accounts[index.toString()]; + const applicationStore = storage.application; + const chainStore = storage.getChainStore(network); - localAccountStore.blockHash = blockHeader.id; + applicationStore.blockHash = blockHeader.id; - storage.importBlockHeader(blockHeader); + chainStore.importBlockHeader(blockHeader); logger.silly(`Account.importBlockHeader(${blockHeader.id})`); }; diff --git a/src/types/Account/methods/importTransactions.js b/src/types/Account/methods/importTransactions.js index 2e3bb0dd1..be26ef68d 100644 --- a/src/types/Account/methods/importTransactions.js +++ b/src/types/Account/methods/importTransactions.js @@ -8,30 +8,36 @@ const ensureAddressesToGapLimit = require('../../../utils/bip44/ensureAddressesT * @param transactions * @returns {Promise} */ -module.exports = async function importTransactions(transactions) { +module.exports = async function importTransactions(transactionsWithMayBeMetadata) { const { - walletType, - walletId, - index, - store, storage, - getAddress, + network, + walletId, + accountPath, + keyChainStore, } = this; - const localWalletStore = store.wallets[walletId]; + const chainStore = storage.getChainStore(network); + const accountStore = storage + .getWalletStore(walletId) + .getPathState(accountPath); - storage.importTransactions(transactions); - logger.silly(`Account.importTransactions(len: ${transactions.length})`); + const masterKeyChain = keyChainStore.getMasterKeyChain(); - if ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(walletType)) { - // After each imports, we will need to ensure we keep our gap of 20 unused addresses - return ensureAddressesToGapLimit( - localWalletStore, - walletType, - index, - getAddress.bind(this), - ); - } + transactionsWithMayBeMetadata.forEach(([transaction, metadata]) => { + const affectedAddressesData = chainStore.importTransaction(transaction, metadata); + const affectedAddresses = Object.keys(affectedAddressesData); + logger.silly(`Account.importTransactions - Import ${transaction.hash} to chainStore. ${affectedAddresses.length} addresses affected.`); + affectedAddresses.forEach((address) => { + const issuedPaths = masterKeyChain.markAddressAsUsed(address); + logger.silly(`Account.importTransactions - newly issued paths ${issuedPaths.length}`); + issuedPaths.forEach((issuedPath) => { + accountStore.addresses[issuedPath.path] = issuedPath.address.toString(); + chainStore.importAddress(issuedPath.address.toString()); + }); + }); + }); + logger.silly(`Account.importTransactions(len: ${transactionsWithMayBeMetadata.length})`); return 0; }; diff --git a/src/types/Wallet/Wallet.js b/src/types/Wallet/Wallet.js index c2eb9d744..2e11fb7ca 100644 --- a/src/types/Wallet/Wallet.js +++ b/src/types/Wallet/Wallet.js @@ -88,21 +88,21 @@ class Wallet extends EventEmitter { mnemonic = generateNewMnemonic(); createdFromNewMnemonic = true; } - this.fromMnemonic(mnemonic); + this.fromMnemonic(mnemonic, this.network, this.passphrase); } else if ('seed' in opts) { - this.fromSeed(opts.seed); + this.fromSeed(opts.seed, this.network); } else if ('HDPrivateKey' in opts) { this.fromHDPrivateKey(opts.HDPrivateKey); } else if ('privateKey' in opts) { this.fromPrivateKey((opts.privateKey === null) ? new PrivateKey(network).toString() - : opts.privateKey); + : opts.privateKey, this.network); } else if ('publicKey' in opts) { - this.fromPublicKey(opts.publicKey); + this.fromPublicKey(opts.publicKey, this.network); } else if ('HDPublicKey' in opts) { this.fromHDPublicKey(opts.HDPublicKey); } else if ('address' in opts) { - this.fromAddress(opts.address); + this.fromAddress(opts.address, this.network); } else { this.fromMnemonic(generateNewMnemonic()); createdFromNewMnemonic = true; @@ -114,21 +114,22 @@ class Wallet extends EventEmitter { this.storage = new Storage({ rehydrate: true, autosave: true, - network, }); this.storage.configure({ adapter: opts.adapter, }); - this.store = this.storage.store; + this.storage.application.network = this.network; + this.storage.createWalletStore(this.walletId); + this.storage.createChainStore(this.network); if (createdFromNewMnemonic) { // As it is pretty complicated to pass any of wallet options // to a specific plugin, using `store` as an options mediator // is easier. - this.store.syncOptions = { + this.storage.application.syncOptions = { skipSynchronization: true, }; @@ -137,7 +138,7 @@ class Wallet extends EventEmitter { + ' created from the new mnemonic'); } } else if (this.unsafeOptions.skipSynchronizationBeforeHeight) { - this.store.syncOptions = { + this.storage.application.syncOptions = { skipSynchronizationBeforeHeight: this.unsafeOptions.skipSynchronizationBeforeHeight, }; } diff --git a/src/types/Wallet/methods/fromAddress.js b/src/types/Wallet/methods/fromAddress.js index 44cdc0558..2397a91aa 100644 --- a/src/types/Wallet/methods/fromAddress.js +++ b/src/types/Wallet/methods/fromAddress.js @@ -6,13 +6,13 @@ const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); /** * @param address */ -module.exports = function fromAddress(address) { +module.exports = function fromAddress(address, network) { if (!is.address(address)) throw new Error('Expected a valid address (typeof Address or String)'); this.walletType = WALLET_TYPES.ADDRESS; this.mnemonic = null; this.address = address.toString(); - const keyChain = new KeyChain({ address }); + const keyChain = new KeyChain({ address, network }); this.keyChainStore = new KeyChainStore(); this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/src/types/Wallet/methods/fromMnemonic.js b/src/types/Wallet/methods/fromMnemonic.js index fe9a1cdb2..1e2cbdda7 100644 --- a/src/types/Wallet/methods/fromMnemonic.js +++ b/src/types/Wallet/methods/fromMnemonic.js @@ -10,14 +10,16 @@ const { WALLET_TYPES } = require('../../../CONSTANTS'); * Will set a wallet to work with a mnemonic (keychain, walletType & HDPrivateKey) * @param mnemonic */ -module.exports = function fromMnemonic(mnemonic) { +module.exports = function fromMnemonic(mnemonic, network, passphrase = '') { if (!is.mnemonic(mnemonic)) { throw new Error('Expected a valid mnemonic (typeof String or Mnemonic)'); } const trimmedMnemonic = mnemonic.toString().trim(); this.walletType = WALLET_TYPES.HDWALLET; - this.mnemonic = trimmedMnemonic; // todo : What about without this ? - this.HDPrivateKey = mnemonicToHDPrivateKey(trimmedMnemonic, this.network, this.passphrase); + // As we do not require the mnemonic except in this.exportWallet + // users of wallet-lib are free to clear this prop at anytime. + this.mnemonic = trimmedMnemonic; + this.HDPrivateKey = mnemonicToHDPrivateKey(trimmedMnemonic, network, passphrase); this.keyChainStore = new KeyChainStore(); const keyChain = new KeyChain({ HDPrivateKey: this.HDPrivateKey }); diff --git a/src/types/Wallet/methods/fromPrivateKey.js b/src/types/Wallet/methods/fromPrivateKey.js index 36f8c2752..870af9937 100644 --- a/src/types/Wallet/methods/fromPrivateKey.js +++ b/src/types/Wallet/methods/fromPrivateKey.js @@ -7,13 +7,13 @@ const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); * Will set a wallet to work with a mnemonic (keychain, walletType & HDPrivateKey) * @param privateKey */ -module.exports = function fromPrivateKey(privateKey) { +module.exports = function fromPrivateKey(privateKey, network) { if (!is.privateKey(privateKey)) throw new Error('Expected a valid private key (typeof PrivateKey or String)'); this.walletType = WALLET_TYPES.PRIVATEKEY; this.mnemonic = null; this.privateKey = privateKey; - const keyChain = new KeyChain({ privateKey }); + const keyChain = new KeyChain({ privateKey, network }); this.keyChainStore = new KeyChainStore(); this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/src/types/Wallet/methods/fromPublicKey.js b/src/types/Wallet/methods/fromPublicKey.js index ed00b585e..ffbf41344 100644 --- a/src/types/Wallet/methods/fromPublicKey.js +++ b/src/types/Wallet/methods/fromPublicKey.js @@ -7,13 +7,13 @@ const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); * Will set a wallet to work with a mnemonic (keychain, walletType & HDPrivateKey) * @param privateKey */ -module.exports = function fromPublicKey(publicKey) { +module.exports = function fromPublicKey(publicKey, network) { if (!is.publicKey(publicKey)) throw new Error('Expected a valid public key (typeof PublicKey or String)'); this.walletType = WALLET_TYPES.PUBLICKEY; this.mnemonic = null; this.publicKey = publicKey; - const keyChain = new KeyChain({ publicKey }); + const keyChain = new KeyChain({ publicKey, network }); this.keyChainStore = new KeyChainStore(); this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/src/types/Wallet/methods/fromSeed.js b/src/types/Wallet/methods/fromSeed.js index 978adc9ec..038fc138e 100644 --- a/src/types/Wallet/methods/fromSeed.js +++ b/src/types/Wallet/methods/fromSeed.js @@ -8,7 +8,7 @@ const { * fixme: Term seed is often use, but we might want to rename to fromHDPrivateKey * @param seed */ -module.exports = function fromSeed(seed) { +module.exports = function fromSeed(seed, network) { if (!is.seed(seed)) throw new Error('Expected a valid seed (typeof string)'); - return this.fromHDPrivateKey(seedToHDPrivateKey(seed, this.network)); + return this.fromHDPrivateKey(seedToHDPrivateKey(seed, network)); }; diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 103a3e8fa..1673d707e 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -91,13 +91,13 @@ export declare type broadcastTransactionOpts = T & { export declare type AddressInfo = T & { path: string; address: string; - balanceSat: number; index: number; - fetchedLast:number; + transaction: [object]; + balanceSat: number; unconfirmedBalanceSat: number; - transaction: object; - used:boolean; utxos:[object] + fetchedLast:number; + used:boolean; } export declare type Network = "livenet" | "testnet" | "evonet" | "regtest" | "local" | "devnet" | "mainnet"; diff --git a/src/utils/bip44/ensureAddressesToGapLimit.js b/src/utils/bip44/ensureAddressesToGapLimit.js index 020fab42e..a4c84cbc7 100644 --- a/src/utils/bip44/ensureAddressesToGapLimit.js +++ b/src/utils/bip44/ensureAddressesToGapLimit.js @@ -86,7 +86,6 @@ function ensureAccountAddressesToGapLimit(walletStore, walletType, accountIndex, gapBetweenLastUsedAndLastGenerated.internal = lastGeneratedIndexes.internal - lastUsedIndexes.internal; addressesToGenerate.internal = BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.internal; } - Object.entries(addressesToGenerate) .forEach(([typeToGenerate, numberToGenerate]) => { if (numberToGenerate > 0) { diff --git a/src/utils/calculateDuffBalance.js b/src/utils/calculateDuffBalance.js new file mode 100644 index 000000000..b5c7da969 --- /dev/null +++ b/src/utils/calculateDuffBalance.js @@ -0,0 +1,62 @@ +/** + * + * @param walletId - The wallet Id where to perform the calculation + * @param accountIndex - The account Index where to perform the calculation + * @param type {{'confirmed','unconfirmed','total'}} Default: total. Calculate balance by utxo type. + * @return {number} Balance in duff + */ +module.exports = function calculateDuffBalance(addresses, chainStore, type = 'total') { + let totalSat = 0; + + addresses.forEach((address) => { + const addressData = chainStore.getAddress(address); + switch (type) { + case 'total': + totalSat += addressData.balanceSat + addressData.unconfirmedBalanceSat; + break; + case 'confirmed': + totalSat += addressData.balanceSat; + break; + case 'unconfirmed': + totalSat += addressData.unconfirmedBalanceSat; + break; + default: + throw new Error(`Unexpected balance type. Got ${type}`); + } + }); + // if (walletId === undefined || accountIndex === undefined) { + // throw new Error('Cannot calculate without walletId and accountIndex params'); + // } + // + // const { addresses } = this.getStore().wallets[walletId]; + // const subwallets = Object.keys(addresses); + // subwallets.forEach((subwallet) => { + // const paths = Object.keys(addresses[subwallet]) + // // We filter out other potential account + // .filter((el) => { + // const splitted = el.split('/'); + // const index = parseInt((splitted.length === 1) ? splitted[0] : splitted[3], 10); + // return index === accountIndex; + // }); + // + // paths.forEach((path) => { + // const address = addresses[subwallet][path]; + // const { balanceSat, unconfirmedBalanceSat } = address; + // switch (type) { + // case 'total': + // totalSat += balanceSat + unconfirmedBalanceSat; + // break; + // case 'confirmed': + // totalSat += balanceSat; + // break; + // case 'unconfirmed': + // totalSat += unconfirmedBalanceSat; + // break; + // default: + // throw new Error(`Unexpected balance type. Got ${type}`); + // } + // }); + // }); + + return totalSat; +}; diff --git a/src/utils/index.js b/src/utils/index.js index ac209d5d5..c23bb6a6e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,6 +1,7 @@ const extendTransactionsWithMetadata = require('./extendTransactionsWithMetadata'); const calculateTransactionFees = require('./calculateTransactionFees'); const categorizeTransactions = require('./categorizeTransactions'); +const calculateDuffBalance = require('./calculateDuffBalance'); const filterTransactions = require('./filterTransactions'); const { hash, doubleSha256, sha256 } = require('./crypto'); const { varIntSizeBytesFromLength } = require('./varInt'); @@ -29,6 +30,7 @@ module.exports = { calculateTransactionFees, categorizeTransactions, mnemonicToHDPrivateKey, + calculateDuffBalance, generateNewMnemonic, seedToHDPrivateKey, mnemonicToWalletId, diff --git a/tests/functional/chainStore.spec.js b/tests/functional/chainStore.spec.js new file mode 100644 index 000000000..3a7aedefd --- /dev/null +++ b/tests/functional/chainStore.spec.js @@ -0,0 +1,153 @@ +const { expect } = require('chai'); +const { ChainStore } = require('../../src'); +const { BlockHeader,Transaction } = require("@dashevo/dashcore-lib"); + +let testnetChainStore; +const testnetBlockHeadersFixtures = require('../../fixtures/chains/testnet/blockheaders.json'); +const testnetTransactionsFixtures = require('../../fixtures/chains/testnet/transactions.json'); + +describe('ChainStore - Functional', ()=>{ + describe('simple usage', ()=>{ + it('should create a testnet chain store', function () { + testnetChainStore = new ChainStore('testnet'); + expect(testnetChainStore.network).to.equal('testnet') + }); + it('should have a initial state', function () { + expect(testnetChainStore.state.blockHeight).to.equal(0); + expect(testnetChainStore.state.fees).to.deep.equal({ + minRelay: -1 + }); + }); + it('should allow to import a blockHeaders', function () { + const stringifiedBlockHeader = testnetBlockHeadersFixtures[0].blockheader; + const blockHeader = new BlockHeader(Buffer.from(stringifiedBlockHeader, 'hex')); + testnetChainStore.importBlockHeader(blockHeader) + expect(testnetChainStore.getBlockHeader(blockHeader.hash).toString()).to.equal(stringifiedBlockHeader) + }); + it('should allow to watch for an address state', function () { + testnetChainStore.importAddress('yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq'); + const expectedGetAddress = { + "address": "yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq", + "balanceSat": 0, + "transactions": [], + "unconfirmedBalanceSat": 0, + "utxos": {} + } + expect(testnetChainStore.getAddress('yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq')).to.deep.equal(expectedGetAddress) + }); + it('should allow to import a transaction with metadata', function () { + const { blockHash, height, isInstantLocked, isChainLocked } = testnetTransactionsFixtures[0] + const stringifiedTransaction = testnetTransactionsFixtures[0].transaction; + const metadata = { + blockHash, + height, + isInstantLocked, + isChainLocked + } + const transaction = new Transaction(Buffer.from(stringifiedTransaction, 'hex')); + testnetChainStore.importTransaction(transaction, metadata) + expect(testnetChainStore.getTransaction(transaction.hash).transaction.toString()).to.equal(stringifiedTransaction) + expect(testnetChainStore.getTransaction(transaction.hash).metadata).to.deep.equal({ + blockHash, + height, + isInstantLocked, + isChainLocked + }) + }); + it('should have update address state', function () { + const expectedGetAddress = { + "address": "yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq", + "balanceSat": 114820000, + "transactions": [ + "7a3c3401462d5dc116db799edb768a2cc0c5e8c05c5483f052b6cab08367a353", + ], + "unconfirmedBalanceSat": 0, + "utxos": { + "7a3c3401462d5dc116db799edb768a2cc0c5e8c05c5483f052b6cab08367a353-0": { + "satoshis": 114820000, + "script": "76a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac" + } + } + } + expect(testnetChainStore.getAddress('yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq')).to.deep.equal(expectedGetAddress) + }); + it('should allow to import a transaction without metadata', function () { + const stringifiedTransaction = testnetTransactionsFixtures[1].transaction; + const transaction = new Transaction(Buffer.from(stringifiedTransaction, 'hex')); + testnetChainStore.importTransaction(transaction) + expect(testnetChainStore.getTransaction(transaction.hash).transaction.toString()).to.equal(stringifiedTransaction) + expect(testnetChainStore.getTransaction(transaction.hash).metadata).to.deep.equal({ + blockHash: null, + height: null, + isInstantLocked: null, + isChainLocked: null + }) + }); + it('should have update address state', function () { + const expectedGetAddress = { + "address": "yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq", + "balanceSat": 214890000, + "transactions": [ + "7a3c3401462d5dc116db799edb768a2cc0c5e8c05c5483f052b6cab08367a353", + "61e5a9ffbf505ad9e5b0a715673ec3c89d68dc9b1d1af8fd980240b8ac14c29c" + ], + "unconfirmedBalanceSat": 0, + "utxos": { + "61e5a9ffbf505ad9e5b0a715673ec3c89d68dc9b1d1af8fd980240b8ac14c29c-0": { + "satoshis": 100070000, + "script": "76a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac" + }, + "7a3c3401462d5dc116db799edb768a2cc0c5e8c05c5483f052b6cab08367a353-0": { + "satoshis": 114820000, + "script": "76a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac" + } + } + } + expect(testnetChainStore.getAddress('yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq')).to.deep.equal(expectedGetAddress) + }); + it('should update a transaction', function () { + const stringifiedTransaction = testnetTransactionsFixtures[1].transaction; + const transaction = new Transaction(Buffer.from(stringifiedTransaction, 'hex')); + testnetChainStore.importTransaction(transaction, { + blockHash: '0000005c81a683007e86e75c76b4b2feca229f806702ca92953562f2ae628ce7', + height: 618023, + isInstantLocked: true, + isChainLocked: true + }) + expect(testnetChainStore.getTransaction(transaction.hash).transaction.toString()).to.equal(stringifiedTransaction) + expect(testnetChainStore.getTransaction(transaction.hash).metadata).to.deep.equal({ + blockHash: '0000005c81a683007e86e75c76b4b2feca229f806702ca92953562f2ae628ce7', + height: 618023, + isInstantLocked: true, + isChainLocked: true + }) + }); + // it('should also have updated watched address', function () { + // + // }); + // it('should allow to import an instantLock for a transaction', function () { + // + // }); + // it('should have updated address state with locks', function () { + // + // }); + let exportedState; + it('should allow to export', function () { + exportedState = testnetChainStore.exportState() + }); + it('should allow to import store', function () { + const importedChainStore = new ChainStore(); + importedChainStore.importState(exportedState) + expect(importedChainStore.exportState()).to.deep.equal(exportedState) + }); + it('should consider previous transaction when address is added afterwards', function () { + const importedChainStore = new ChainStore(); + const modifiedState = JSON.parse(JSON.stringify(exportedState)); + modifiedState.state.addresses = {}; + importedChainStore.importState(modifiedState) + importedChainStore.importAddress("yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq") + expect(importedChainStore.exportState()).to.deep.equal(exportedState) + }); + }) +}); + diff --git a/tests/functional/identitiesStore.spec.js b/tests/functional/identitiesStore.spec.js new file mode 100644 index 000000000..ac8c2d83b --- /dev/null +++ b/tests/functional/identitiesStore.spec.js @@ -0,0 +1,24 @@ +//[ +// "9Gk9T5mJY9j3dDX1D1tG5WYaV8g6zQTS2ocFFXe6NCrq", +// null, +// "HZJywfYZ87fdJFLkp7wtnTfS29zpvR63f21gqaajLYx6" +// ] + +const { expect } = require('chai'); +const { IdentitiesStore } = require('../../src'); +// const { BlockHeader,Transaction } = require("@dashevo/dashcore-lib"); +// +let identitiesStore; +// const testnetBlockHeadersFixtures = require('../../fixtures/chains/testnet/blockheaders.json'); +// const testnetTransactionsFixtures = require('../../fixtures/chains/testnet/transactions.json'); + +describe('IdentitiesStore - Functional', ()=> { + describe('simple usage', () => { + it('should create an identityStore', function () { + identitiesStore = new IdentitiesStore(); + }); + it('should import an identity', function () { + identitiesStore.importIdentity() + }); + }) +}) \ No newline at end of file diff --git a/tests/functional/offlineWallet.spec.js b/tests/functional/offlineWallet.spec.js new file mode 100644 index 000000000..3da8930c6 --- /dev/null +++ b/tests/functional/offlineWallet.spec.js @@ -0,0 +1,83 @@ +const { expect, use} = require('chai'); +const { Wallet } = require('../../src/index'); +const { Transaction } = require('@dashevo/dashcore-lib'); +let offlineWallet; +let account0; +describe('Wallet-lib - functional - offline Wallet', function suite() { + this.timeout(700000); + describe('Wallet', () => { + describe('Create a new offline wallet', () => { + it('should create a new offline wallet', () => { + offlineWallet = new Wallet({ + offlineMode: true, + mnemonic: 'replace eternal resource drill side kidney sudden thought account fog fluid wire' + }); + }); + }); + }); + + describe('Account', () => { + it('should allow to get the first account', async function () { + account0 = await offlineWallet.getAccount(); + expect(account0.offlineMode).to.equal(true) + expect(account0.accountPath).to.equal(`m/44'/1'/0'`) + }); + it('should get all addresses', function () { + const externalAddressesSet = account0.getAddresses(); + const internalAddressesSet = account0.getAddresses('internal'); + expect(Object.keys(externalAddressesSet)).to.deep.equal([ + 'm/0/0', 'm/0/1', 'm/0/2', + 'm/0/3', 'm/0/4', 'm/0/5', + 'm/0/6', 'm/0/7', 'm/0/8', + 'm/0/9', 'm/0/10', 'm/0/11', + 'm/0/12', 'm/0/13', 'm/0/14', + 'm/0/15', 'm/0/16', 'm/0/17', + 'm/0/18', 'm/0/19' + ]) + expect(Object.keys(internalAddressesSet)).to.deep.equal([ + 'm/1/0', 'm/1/1', 'm/1/2', + 'm/1/3', 'm/1/4', 'm/1/5', + 'm/1/6', 'm/1/7', 'm/1/8', + 'm/1/9', 'm/1/10', 'm/1/11', + 'm/1/12', 'm/1/13', 'm/1/14', + 'm/1/15', 'm/1/16', 'm/1/17', + 'm/1/18', 'm/1/19' + ]) + }); + it('should generate new address on tx affecting address', function () { + expect(account0.getUnusedAddress()).to.deep.equal({ + address: 'yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq', + path: 'm/0/0', + index: 0 + }); + const transactionsWithMetadata = [ + [ + new Transaction('0200000001c7d66bb85e0069c221b44b07f49f52cc4f2e54f70e14430b94888327763a66a9010000006b483045022100da5b319f73e6adfee751f33308f5a8c1fceeab2683e15e132d79053b3118639602204262022fb85f88d9802649a289a1134b678efcf708faaeae8f101e8eab785054012102bc626898b49f31f5194de7bc68004401639a20cfa82e4c2eac9684a91fc47a57feffffff0270f2f605000000001976a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac912e250c250000001976a91415e1edb5c5d9e67d0e36f94343b3eff26bb76d1088ac266e0900'), + {"blockHash":"0000005c81a683007e86e75c76b4b2feca229f806702ca92953562f2ae628ce7","height":618023,"instantLocked":true,"chainLocked":true} + ] + ] + account0.importTransactions(transactionsWithMetadata); + expect(account0.getUnusedAddress()).to.deep.equal({ + address: 'yWFHBxc6c9jmkL82v795PtJJteSkcVKbt5', + path: 'm/0/1', + index: 1 + }); + expect(account0.getTotalBalance()).to.equal(100070000); + expect(account0.getUTXOS().length).to.equal(1); + expect(account0.getUTXOS()[0].toString()).to.equal('61e5a9ffbf505ad9e5b0a715673ec3c89d68dc9b1d1af8fd980240b8ac14c29c:0'); + expect(account0.getUTXOS()[0]).to.deep.equal(new Transaction.UnspentOutput({"address":"yVSuCVTGpViqV8bzG3kdtofkPhSRWH8dbq","txid":"61e5a9ffbf505ad9e5b0a715673ec3c89d68dc9b1d1af8fd980240b8ac14c29c","vout":0,"scriptPubKey":"76a91464220a1c12690ec26d837b3be0a2e3588bb4b79188ac","amount":1.0007})) + }); + it('should have issued new addresses', function () { + const externalAddressesSet = account0.getAddresses(); + expect(Object.keys(externalAddressesSet)).to.deep.equal([ + 'm/0/0', 'm/0/1', 'm/0/2', + 'm/0/3', 'm/0/4', 'm/0/5', + 'm/0/6', 'm/0/7', 'm/0/8', + 'm/0/9', 'm/0/10', 'm/0/11', + 'm/0/12', 'm/0/13', 'm/0/14', + 'm/0/15', 'm/0/16', 'm/0/17', + 'm/0/18', 'm/0/19', 'm/0/20' + ]) + }); + }); +}); diff --git a/tests/functional/storage.spec.js b/tests/functional/storage.spec.js new file mode 100644 index 000000000..3fb7868c6 --- /dev/null +++ b/tests/functional/storage.spec.js @@ -0,0 +1,35 @@ +const { expect } = require('chai'); +const { Storage } = require('../../src'); + +let storage; +describe('Storage - Functional', ()=> { + describe('simple usage', () => { + it('should create a storage', function () { + storage = new Storage(); + }); + it('should create a chain', function () { + storage.createChainStore('testnet') + }); + it('should get a chain store', function () { + const chainStore = storage.getChainStore('testnet') + expect(chainStore.network).to.equal('testnet'); + expect(chainStore.state.blockHeight).to.equal(0); + }); + it('should create a wallet', function () { + storage.createWalletStore('73a5575413') + }); + it('should get a wallet store', function () { + const walletStore = storage.getWalletStore('73a5575413') + expect(walletStore.walletId).to.equal('73a5575413'); + }); + it('should create paths store', function () { + const walletStore = storage.getWalletStore('73a5575413') + // const publicKeyStore = walletStore.createPathStore(`m/0'`); + const accountStore = walletStore.createPathState(`m/44'/1'/0'`); + // const contactReceivingStore = walletStore.createPathStore(`m/9'/1'/15'/0'/123/456`); + }); + it('should ', function () { + + }); + }) +}) \ No newline at end of file diff --git a/tests/functional/walletStore.spec.js b/tests/functional/walletStore.spec.js new file mode 100644 index 000000000..61fbaa292 --- /dev/null +++ b/tests/functional/walletStore.spec.js @@ -0,0 +1,14 @@ +const { expect } = require('chai'); +const { WalletStore } = require('../../src'); + +let walletStore; +describe('WalletStore - Functional', ()=> { + describe('simple usage', () => { + it('should create a walletStore', function () { + walletStore = new WalletStore(); + }); + it('should create account', function () { + walletStore.createAccount() + }); + }) +}) \ No newline at end of file