From 4e16dcdd9b92c6f7e5278c6440c1f9f663eec664 Mon Sep 17 00:00:00 2001 From: Alex Werner Date: Thu, 13 Jan 2022 10:25:10 +0100 Subject: [PATCH 1/3] feat: KeyChain and KeyChainStore --- .../src/types/Account/methods/encrypt.spec.js | 2 +- .../types/Account/methods/generateAddress.js | 72 +++++++--- .../src/types/Account/methods/getAddress.js | 46 +++--- .../src/types/Identities/Identities.js | 2 +- .../methods/getIdentityHDKeyById.js | 5 +- .../methods/getIdentityHDKeyById.spec.js | 8 +- .../Identities/methods/getIdentityIds.spec.js | 2 - .../wallet-lib/src/types/KeyChain/KeyChain.js | 131 +++++++++++++----- .../src/types/KeyChain/KeyChain.spec.js | 121 +++++++++++----- .../KeyChain/methods/generateKeyForChild.js | 19 --- .../KeyChain/methods/generateKeyForPath.js | 19 --- .../KeyChain/methods/getDIP15ExtendedKey.js | 2 +- .../KeyChain/methods/getFirstUnusedAddress.js | 14 ++ .../types/KeyChain/methods/getForAddress.js | 12 ++ .../src/types/KeyChain/methods/getForPath.js | 42 ++++++ .../KeyChain/methods/getHardenedBIP44HDKey.js | 5 +- .../methods/getHardenedDIP9FeatureHDKey.js | 5 +- .../types/KeyChain/methods/getIssuedPaths.js | 5 + .../types/KeyChain/methods/getKeyForChild.js | 10 -- .../types/KeyChain/methods/getKeyForPath.js | 29 ---- .../types/KeyChain/methods/getPrivateKey.js | 18 --- .../src/types/KeyChain/methods/getRootKey.js | 5 + .../KeyChain/methods/getWatchedAddresses.js | 7 + .../KeyChain/methods/markAddressAsUsed.js | 18 +++ .../types/KeyChain/methods/maybeLookAhead.js | 84 +++++++++++ .../src/types/KeyChainStore/KeyChainStore.js | 16 +++ .../types/KeyChainStore/KeyChainStore.spec.js | 47 +++++++ .../KeyChainStore/methods/addKeyChain.js | 15 ++ .../KeyChainStore/methods/getKeyChain.js | 5 + .../KeyChainStore/methods/getKeyChains.js | 5 + .../methods/getMasterKeyChain.js | 6 + .../methods/makeChildKeyChainStore.js | 21 +++ .../src/types/Wallet/Wallet.spec.js | 17 ++- .../src/types/Wallet/methods/fromAddress.js | 8 +- .../types/Wallet/methods/fromAddress.spec.js | 14 +- .../types/Wallet/methods/fromHDPrivateKey.js | 6 +- .../Wallet/methods/fromHDPrivateKey.spec.js | 7 +- .../types/Wallet/methods/fromHDPublicKey.js | 6 +- .../Wallet/methods/fromHDPublicKey.spec.js | 49 ++++--- .../src/types/Wallet/methods/fromMnemonic.js | 14 +- .../types/Wallet/methods/fromMnemonic.spec.js | 47 +++---- .../types/Wallet/methods/fromPrivateKey.js | 8 +- .../Wallet/methods/fromPrivateKey.spec.js | 12 +- .../src/types/Wallet/methods/fromPublicKey.js | 8 +- .../Wallet/methods/fromPublicKey.spec.js | 12 +- .../src/types/Wallet/methods/fromSeed.js | 4 +- .../src/types/Wallet/methods/fromSeed.spec.js | 14 +- .../utils/bip44/ensureAddressesToGapLimit.js | 10 +- 48 files changed, 705 insertions(+), 329 deletions(-) delete mode 100644 packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js delete mode 100644 packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getForAddress.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getForPath.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getIssuedPaths.js delete mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js delete mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js delete mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getRootKey.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getWatchedAddresses.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/methods/addKeyChain.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChain.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChains.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/methods/getMasterKeyChain.js create mode 100644 packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js diff --git a/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js b/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js index 89653f84bdc..facc317ea96 100644 --- a/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js +++ b/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js @@ -29,7 +29,7 @@ describe('Account - encrypt', function suite() { const secret = 'secret'; it('should encrypt extPubKey with aes', () => { - const extPubKey = account.keyChain.getKeyForPath(derivationPath, 'HDPublicKey').toString(); + const extPubKey = account.keyChainStore.getMasterKeyChain().getForPath(derivationPath).key.toString(); const encryptedExtPubKey = account.encrypt('aes', extPubKey, secret).toString(); const bytes = CryptoJS.AES.decrypt(encryptedExtPubKey, secret); const decrypted = bytes.toString(CryptoJS.enc.Utf8); diff --git a/packages/wallet-lib/src/types/Account/methods/generateAddress.js b/packages/wallet-lib/src/types/Account/methods/generateAddress.js index 450f582fe73..6d19109c108 100644 --- a/packages/wallet-lib/src/types/Account/methods/generateAddress.js +++ b/packages/wallet-lib/src/types/Account/methods/generateAddress.js @@ -1,58 +1,94 @@ -const { PublicKey } = require('@dashevo/dashcore-lib'); const EVENTS = require('../../../EVENTS'); const { WALLET_TYPES } = require('../../../CONSTANTS'); const { is } = require('../../../utils'); + /** * Generate an address from a path and import it to the store * @param {string} path - * @return {AddressObj} Address information + * @param {boolean} [isWatchedAddress=true] - if the address will be watched + * @return {AddressInfo} Address information * */ -function generateAddress(path) { +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 keyPathData; const { network } = this; switch (this.walletType) { case WALLET_TYPES.ADDRESS: - address = this.keyChain.address; + address = this.keyChainStore.getMasterKeyChain().rootKey; + if (isWatchedAddress) { + this.keyChainStore.issuedPaths.set(0, { + path: 0, + address, + isUsed: false, + isWatched: true, + }); + } break; case WALLET_TYPES.PUBLICKEY: - address = new PublicKey(this.keyChain.publicKey.toString()).toAddress(network).toString(); + // eslint-disable-next-line no-case-declarations + const { rootKey } = this.keyChainStore.getMasterKeyChain(); + address = rootKey.toAddress(network).toString(); + if (isWatchedAddress) { + this.keyChainStore.issuedPaths.set(0, { + key: rootKey, + path: 0, + address, + isUsed: false, + isWatched: true, + }); + } break; + case WALLET_TYPES.HDPRIVATE: case WALLET_TYPES.HDWALLET: // eslint-disable-next-line prefer-destructuring - index = parseInt(path.toString().split('/')[5], 10); - privateKey = this.keyChain.getKeyForPath(path); - address = privateKey.publicKey.toAddress(network).toString(); + index = parseInt(path.toString().split('/')[2], 10); + keyPathData = this.keyChainStore + .getMasterKeyChain() + .getForPath(path, { isWatched: isWatchedAddress }); + address = keyPathData.address.toString(); break; case WALLET_TYPES.HDPUBLIC: index = parseInt(path.toString().split('/')[5], 10); - privateKey = this.keyChain.getKeyForChild(index); - address = privateKey.publicKey.toAddress(network).toString(); + // eslint-disable-next-line no-case-declarations + keyPathData = this.keyChainStore + .getMasterKeyChain() + .getForPath(path, { isWatched: isWatchedAddress }); + address = keyPathData.address.toString(); break; + // TODO: DEPRECATE USAGE OF SINGLE_ADDRESS in favor or PRIVATEKEY + case WALLET_TYPES.PRIVATEKEY: case WALLET_TYPES.SINGLE_ADDRESS: default: - privateKey = this.keyChain.getKeyForPath(path.toString()); - address = privateKey.publicKey.toAddress(network).toString(); + keyPathData = this.keyChainStore + .getMasterKeyChain() + .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; } + module.exports = generateAddress; diff --git a/packages/wallet-lib/src/types/Account/methods/getAddress.js b/packages/wallet-lib/src/types/Account/methods/getAddress.js index d4771dc7738..86efad07386 100644 --- a/packages/wallet-lib/src/types/Account/methods/getAddress.js +++ b/packages/wallet-lib/src/types/Account/methods/getAddress.js @@ -1,40 +1,26 @@ const { WALLET_TYPES } = require('../../../CONSTANTS'); -const getTypePathFromWalletType = (walletType, addressType = 'external', index, BIP44PATH) => { - let type; - let path; - - const addressTypeIndex = (addressType === 'external') ? 0 : 1; - switch (walletType) { - case WALLET_TYPES.HDWALLET: - type = addressType; - path = `${BIP44PATH}/${addressTypeIndex}/${index}`; - break; - case WALLET_TYPES.HDPUBLIC: - type = 'external'; - path = `${BIP44PATH}/${addressTypeIndex}/${index}`; - 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(index = 0, _type = 'external') { - const { type, path } = getTypePathFromWalletType(this.walletType, _type, index, this.BIP44PATH); +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/packages/wallet-lib/src/types/Identities/Identities.js b/packages/wallet-lib/src/types/Identities/Identities.js index f721356dcce..f457fe9fc99 100644 --- a/packages/wallet-lib/src/types/Identities/Identities.js +++ b/packages/wallet-lib/src/types/Identities/Identities.js @@ -10,7 +10,7 @@ class Identities { this.storage = wallet.storage; - this.keyChain = wallet.keyChain; + this.keyChain = wallet.keyChainStore.getMasterKeyChain(); } } diff --git a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js index 374925a14a3..79cd6eedc77 100644 --- a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js +++ b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js @@ -5,7 +5,10 @@ * @return {HDPrivateKey} */ function getIdentityHDKeyById(identityId, keyIndex) { - const identityIndex = this.storage.getIndexedIdentityIds(this.walletId).indexOf(identityId); + const identityIndex = this.storage + .getWalletStore(this.walletId) + .getIndexedIdentityIds() + .indexOf(identityId); if (identityIndex === -1) { throw new Error(`Identity with ID ${identityId} is not associated with wallet, or it's not synced`); diff --git a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js index e13dc087569..2d8f3a2b829 100644 --- a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js +++ b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js @@ -1,7 +1,6 @@ const { expect } = require('chai'); const mockedStore = require('../../../../fixtures/sirentonight-fullstore-snapshot-1562711703'); const getIdentityHDKeyById = require('./getIdentityHDKeyById'); -const searchTransaction = require('../../Storage/methods/searchTransaction'); let walletMock; let fetchTransactionInfoCalledNb = 0; @@ -10,12 +9,15 @@ describe('Wallet#getIdentityHDKeyById', function suite() { this.timeout(10000); before(() => { expectedKeyMock = "123"; + const walletStoreMock = { + getIndexedIdentityIds: () => mockedStore.wallets[Object.keys(mockedStore.wallets)].identityIds + } + const storageMock = { store: mockedStore, getStore: () => mockedStore, mappedAddress: {}, - searchTransaction, - getIndexedIdentityIds: () => mockedStore.wallets[Object.keys(mockedStore.wallets)].identityIds, + getWalletStore: () => walletStoreMock, }; const walletId = Object.keys(mockedStore.wallets)[0]; walletMock = { diff --git a/packages/wallet-lib/src/types/Identities/methods/getIdentityIds.spec.js b/packages/wallet-lib/src/types/Identities/methods/getIdentityIds.spec.js index df90868e4c2..df2e887176b 100644 --- a/packages/wallet-lib/src/types/Identities/methods/getIdentityIds.spec.js +++ b/packages/wallet-lib/src/types/Identities/methods/getIdentityIds.spec.js @@ -1,7 +1,6 @@ const { expect } = require('chai'); const mockedStore = require('../../../../fixtures/sirentonight-fullstore-snapshot-1562711703'); const getIdentityIds = require('./getIdentityIds'); -const searchTransaction = require('../../Storage/methods/searchTransaction'); let mockedWallet; let fetchTransactionInfoCalledNb = 0; @@ -12,7 +11,6 @@ describe('Wallet#getIdentityIds', function suite() { store: mockedStore, getStore: () => mockedStore, mappedAddress: {}, - searchTransaction, getIndexedIdentityIds: () => mockedStore.wallets[Object.keys(mockedStore.wallets)].identityIds, }; const walletId = Object.keys(mockedStore.wallets)[0]; diff --git a/packages/wallet-lib/src/types/KeyChain/KeyChain.js b/packages/wallet-lib/src/types/KeyChain/KeyChain.js index 643dcc62737..74931778764 100644 --- a/packages/wallet-lib/src/types/KeyChain/KeyChain.js +++ b/packages/wallet-lib/src/types/KeyChain/KeyChain.js @@ -1,52 +1,107 @@ const { Networks, HDPrivateKey, HDPublicKey } = require('@dashevo/dashcore-lib'); -const { has } = require('lodash'); +const { PrivateKey, PublicKey } = require('@dashevo/dashcore-lib'); +const { doubleSha256 } = require('../../utils/crypto'); +const { mnemonicToHDPrivateKey } = require('../../utils/mnemonic'); -// eslint-disable-next-line no-underscore-dangle -const _defaultOpts = { - network: Networks.testnet.toString(), - keys: {}, -}; +function generateKeyChainId(key) { + const keyChainIdSuffix = doubleSha256(key.toString()).toString('hex').slice(0, 10); + return `kc${keyChainIdSuffix}`; +} -class KeyChain { - constructor(opts = JSON.parse(JSON.stringify(_defaultOpts))) { - const defaultOpts = JSON.parse(JSON.stringify(_defaultOpts)); - this.network = defaultOpts.network; - this.keys = { ...defaultOpts.keys }; - - if (has(opts, 'HDPrivateKey')) { - this.type = 'HDPrivateKey'; - this.HDPrivateKey = (typeof opts.HDPrivateKey === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; - this.network = this.HDPrivateKey.network; - } else if (has(opts, 'HDPublicKey')) { - this.type = 'HDPublicKey'; - this.HDPublicKey = (typeof opts.HDPublicKey === 'string') ? HDPublicKey(opts.HDPublicKey) : opts.HDPublicKey; - this.network = this.HDPublicKey.network; - } else if (has(opts, 'privateKey')) { - this.type = 'privateKey'; - this.privateKey = opts.privateKey; - } else if (has(opts, 'publicKey')) { - this.type = 'publicKey'; - this.publicKey = opts.publicKey; - } else if (has(opts, 'address')) { - this.type = 'address'; - this.address = opts.address.toString(); - } else { - throw new Error('Expect privateKey, publicKey, HDPublicKey, HDPrivateKey or Address'); +function fromOptions(opts) { + let rootKey; + let rootKeyType; + let network = Networks.testnet.toString(); + let passphrase = ''; + + if (opts) { + if (opts.passphrase) { + passphrase = opts.passphrase; + } + if (opts.mnemonic) { + rootKeyType = 'HDPrivateKey'; + rootKey = (typeof opts.mnemonic === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; + } + if (opts.network) { + network = opts.network; + } + if (opts.HDPrivateKey) { + rootKeyType = 'HDPrivateKey'; + rootKey = (typeof opts.HDPrivateKey === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; + network = rootKey.network.toString(); + } else if (opts.HDPublicKey) { + rootKeyType = 'HDPublicKey'; + rootKey = (typeof opts.HDPublicKey === 'string') ? HDPublicKey(opts.HDPublicKey) : opts.HDPublicKey; + network = rootKey.network.toString(); + } else if (opts.privateKey) { + rootKeyType = 'privateKey'; + rootKey = (typeof opts.privateKey === 'string') ? new PrivateKey(opts.privateKey, opts.network) : opts.privateKey; + network = rootKey.network.toString(); + } else if (opts.publicKey) { + rootKeyType = 'publicKey'; + rootKey = (typeof opts.publicKey === 'string') ? new PublicKey(opts.publicKey, opts.network) : opts.publicKey; + network = rootKey.network.toString(); + } else if (opts.address) { + rootKeyType = 'address'; + rootKey = opts.address.toString(); + } else if (opts.mnemonic) { + return fromOptions({ + ...opts, + HDPrivateKey: mnemonicToHDPrivateKey(opts.mnemonic, network, passphrase), + }); } - if (opts.network) this.network = opts.network; - if (opts.keys) this.keys = { ...opts.keys }; } + + const lookAheadOpts = { + isWatched: true, + paths: {}, + ...opts.lookAheadOpts, + }; + + return { + rootKeyType, + rootKey, + network, + passphrase, + lookAheadOpts, + }; } -KeyChain.prototype.generateKeyForChild = require('./methods/generateKeyForChild'); -KeyChain.prototype.generateKeyForPath = require('./methods/generateKeyForPath'); +class KeyChain { + constructor(opts = {}) { + const { + rootKey, + rootKeyType, + network, + lookAheadOpts, + } = fromOptions(opts); + if (!rootKeyType || !rootKey) { + throw new Error('Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.'); + } + this.keyChainId = generateKeyChainId(rootKey); + + this.rootKey = rootKey; + this.network = network; + this.rootKeyType = rootKeyType; + this.lookAheadOpts = { isWatched: true, ...lookAheadOpts }; + + this.issuedPaths = new Map(); + + this.maybeLookAhead(); + } +} +KeyChain.prototype.getForPath = require('./methods/getForPath'); +KeyChain.prototype.getForAddress = require('./methods/getForAddress'); KeyChain.prototype.getDIP15ExtendedKey = require('./methods/getDIP15ExtendedKey'); +KeyChain.prototype.getFirstUnusedAddress = require('./methods/getFirstUnusedAddress'); KeyChain.prototype.getHardenedBIP44HDKey = require('./methods/getHardenedBIP44HDKey'); KeyChain.prototype.getHardenedDIP9FeatureHDKey = require('./methods/getHardenedDIP9FeatureHDKey'); KeyChain.prototype.getHardenedDIP15AccountKey = require('./methods/getHardenedDIP15AccountKey'); -KeyChain.prototype.getKeyForChild = require('./methods/getKeyForChild'); -KeyChain.prototype.getKeyForPath = require('./methods/getKeyForPath'); -KeyChain.prototype.getPrivateKey = require('./methods/getPrivateKey'); +KeyChain.prototype.getRootKey = require('./methods/getRootKey'); +KeyChain.prototype.getWatchedAddresses = require('./methods/getWatchedAddresses'); +KeyChain.prototype.getIssuedPaths = require('./methods/getIssuedPaths'); +KeyChain.prototype.maybeLookAhead = require('./methods/maybeLookAhead'); +KeyChain.prototype.markAddressAsUsed = require('./methods/markAddressAsUsed'); KeyChain.prototype.sign = require('./methods/sign'); module.exports = KeyChain; diff --git a/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js b/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js index 3cf9bb0b5a1..f392769cdda 100644 --- a/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js +++ b/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js @@ -8,36 +8,32 @@ let keychain2; const mnemonic = 'during develop before curtain hazard rare job language become verb message travel'; const mnemonic2 = 'birth kingdom trash renew flavor utility donkey gasp regular alert pave layer'; const pk = '4226d5e2fe8cbfe6f5beb7adf5a5b08b310f6c4a67fc27826779073be6f5699e'; - +const hdPublicKey = 'xpub661MyMwAqRbcFGB6XSWBsD725rJDUbFUpy4zWe2u22nJ2BxpoHFxtVDfKnTnvVQHohnY7AsVpRTHDv6PyPQTYu1KxFPKw29MAVXPEpz1G7V'; const expectedRootDIP15AccountKey_0 = 'tprv8hRzmheQujhJN5XP2dj955nAFCKeEoSifJRWuutdbwWRtusdDQ426jbp75EqErUSuTxmPyxYmP1TpcF5qdxGhXLNXRLMGsRLG6NFCv1WnaQ'; const expectedRootDIP15AccountKey_1 = 'tprv8hRzmheQujhJQyCtFTuUFHxB3Ag5VLB994zhH4CfxbA41cq73HT2mpYq5M33V54oJyn6g514saxxVJB886G55eYX56J6D6x87UNNT6iQHkR'; const expectedKeyForChild_0 = 'tprv8d4podc2Tg459CH2bwLHXj3vdJFBT2rdsk5Nr1djH7hzHdt5LRdvN6QyFwMiDy7ffRdik7fEVRKKgsHB4F18sh8xF6jFXpKq4sUgGBoSbKw'; describe('Keychain', function suite() { - this.timeout(10000); + this.timeout(1000); it('should create a keychain', () => { - const expectedException1 = 'Expect privateKey, publicKey, HDPublicKey, HDPrivateKey or Address'; + const expectedException1 = 'Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.'; expect(() => new KeyChain()).to.throw(expectedException1); - expect(() => new KeyChain(mnemonic)).to.throw(expectedException1); - keychain = new KeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') }); - expect(keychain.type).to.equal('HDPrivateKey'); + keychain = new KeyChain({ mnemonic: mnemonic, network: 'testnet' }); + expect(keychain.rootKeyType).to.equal('HDPrivateKey'); expect(keychain.network.toString()).to.equal('testnet'); - expect(keychain.keys).to.deep.equal({}); + expect(keychain.rootKey.network.toString()).to.equal('testnet'); - keychain2 = new KeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic2, 'mainnet') }); - }); - it('should get private key', () => { - expect(keychain.getPrivateKey().toString()).to.equal(pk); + keychain2 = new KeyChain({ mnemonic: mnemonic2, network: 'livenet' }); }); it('should generate key for full path', () => { const path = 'm/44\'/1\'/0\'/0/0'; - const pk2 = keychain.getKeyForPath(path); + const pk2 = keychain.getForPath(path).key; const address = new Dashcore.Address(pk2.publicKey.toAddress()).toString(); expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT'); }); it('should get hardened feature path', () => { const hardenedPk = keychain.getHardenedBIP44HDKey(); - const pk2 = keychain.getKeyForPath('m/44\'/1\''); + const pk2 = keychain.getForPath('m/44\'/1\'').key; expect(pk2.toString()).to.equal(hardenedPk.toString()); }); it('should get DIP15 account key', function () { @@ -46,6 +42,7 @@ describe('Keychain', function suite() { const rootDIP15AccountKey_1 = keychain.getHardenedDIP15AccountKey(1); expect(rootDIP15AccountKey_1.toString()).to.deep.equal(expectedRootDIP15AccountKey_1); }); + it('should get DIP15 extended key', function () { const userUniqueId = '0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a'; const contactUniqueId = '0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'; @@ -65,11 +62,10 @@ describe('Keychain', function suite() { const DIP15ExtKey_1 = keychain.getDIP15ExtendedKey(userAhash, userBhash, 0, 0); expect(DIP15ExtKey_1.privateKey.toString()).to.equal('60581b6dca8244d3fb3cfe619b5a22277e5423b01e5285f356981f247e0f4a60') expect(DIP15ExtKey_1.publicKey.toString()).to.equal('03deaac00f721151307fbc7bf80d7b8afab98c1f026d67e5f56b21e2013f551ce6') - }); it('should derive from hardened feature path', () => { const hardenedHDKey = keychain.getHardenedBIP44HDKey(); - const pk2 = keychain.getKeyForPath(`m/44'/1'`); + const pk2 = keychain.getForPath(`m/44'/1'`).key; expect(pk2.toString()).to.equal(hardenedHDKey.toString()); expect(hardenedHDKey.toString()).to.deep.equal('tprv8dtrJNytYHRiZY585hmHGbguS6VjGpK49puSB7oXZjLHcQfrAzQkF4ZCxM2DkEbyY85J4EYcZ8EjT5ZCU8ozB727TDdodbfXet5GkGau2RQ'); const derivedPk = hardenedHDKey.deriveChild(0, true).deriveChild(0).deriveChild(0); @@ -78,29 +74,88 @@ describe('Keychain', function suite() { }); it('should get hardened DIP9FeatureHDKey', function () { const hardenedHDKey = keychain.getHardenedDIP9FeatureHDKey(); - const pk2 = keychain.getKeyForPath(`m/9'/1'`); + const pk2 = keychain.getForPath(`m/9'/1'`).key; expect(pk2.toString()).to.equal(hardenedHDKey.toString()); expect(hardenedHDKey.toString()).to.deep.equal('tprv8fBJjWoGgCpGRCbyzE9RUA59rmoN1RUijhLnXGL4VHnLxvSe523yVg4GrGzbR6TyXtdynAEh5z8UX55EXt2Cb3xjvrsx2PgTY9BHxzFVkWn'); }); - it('should generate key for child', () => { + it('should get key for path', () => { const keychain2 = new KeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') }); - const keyForChild = keychain2.generateKeyForChild(0); + const keyForChild = keychain2.getForPath('m/0').key; expect(keyForChild.toString()).to.equal(expectedKeyForChild_0); }); + it('should mark address watched and get watched addresses', function () { + const key0 = keychain.getForPath('m/0'); + keychain.getForPath('m/0').isWatched = true + key0.isWatched = true; + const key1 = keychain.getForPath('m/1', { isWatched: true }); + const key2 = keychain.getForPath('m/2', { isWatched: true }); - it('should sign', () => { + const watchedAddresses = keychain.getWatchedAddresses(); + let expectedWatchedAddresses = [ + keychain.getForPath('m/0').address.toString(), + keychain.getForPath('m/1').address.toString(), + keychain.getForPath('m/2').address.toString() + ]; + expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); + }); + it('should get watched addresses', function () { + const watchedAddresses = keychain.getWatchedAddresses(); + const expectedWatchedAddresses = [ + 'ybQDfNwiDjk8ZH5UUmHQzAMEmjbrbK5dAj', + 'yhFX5rseJPitV45HUCaa9haeGHtLuooBaq', + 'yhqxsmYk6jfoGWf1hJKq7d4U2cGHCgzpFU' + ] + expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); + }); + // it('should get watched public keys', function () { + // const watchedPubKeys = keychain.getWatchedPublicKeys(); + // const expectedWatchedPubKeys = [ + // '03e6ab8177a7ca2699da4f83ca3c27768fb88b70ae9d6bde1cba8de88355ccf199', + // '0246a870b65153b98e453ff08f7198d06bce2a790286f44d90929aceafafa0673f', + // '025ad16f78f67801e52abe5b512d66e3896d4c2fa3ca3150349437a5dd13519967' + // ] + // expect(watchedPubKeys).to.deep.equal(expectedWatchedPubKeys) + // }); + it('should remove an address from watched addresses', function () { + const data0 = keychain.getForPath('m/0', { isWatched: false }); + const data1 = keychain.getForPath('m/1'); + const data2 = keychain.getForPath('m/2'); + data2.isWatched = false; + expect(keychain.getWatchedAddresses().length).to.equal(1); }); -}); -describe('Keychain - clone', function suite() { - this.timeout(10000); - it('should clone', () => { - const keychain2 = new KeyChain(keychain); - expect(keychain2).to.deep.equal(keychain); - expect(keychain2.keys).to.deep.equal(keychain.keys); + it('should get address for path', function (){ + const address0_1 = keychain.getForPath('m/1').address; + expect(address0_1.toString()).to.equal('yhFX5rseJPitV45HUCaa9haeGHtLuooBaq') + }) + it('should mark address as used', function () { + const address0_0 = keychain.getForPath('m/0').address; + keychain.markAddressAsUsed(address0_0); + expect(keychain.issuedPaths.get('m/0').isUsed).to.equal(true) }); }); +describe('Keychain - HDPublicKey', function suite(){ + let hdpubKeyChain; + it('should initiate from a HDPublicKey', function () { + hdpubKeyChain = new KeyChain({ + HDPublicKey: new Dashcore.HDPublicKey(hdPublicKey), + network: 'testnet' + }); + // As the HDPublicKey starts with xpub, it's livenet and should take priority over our network being set. + expect(hdpubKeyChain.network.toString()).to.equal('livenet'); + expect(hdpubKeyChain.keyChainId).to.equal('kc5059442d66'); + expect(hdpubKeyChain.getRootKey().toString()).to.equal(hdPublicKey); + }); + it('should derivate', function () { + const key0_1 = hdpubKeyChain.getForPath('m/1').key; + expect(key0_1.publicKey.toAddress(hdpubKeyChain.network).toString()).to.equal('XoL5LcBiDWcj6L7fFwytsFoX5Vz7BVXw9w') + }); + it('should get address for path', function (){ + const address0_1 = hdpubKeyChain.getForPath('m/2').address; + expect(address0_1.toString()).to.equal('XwAzpxQKbgebaLiadq1c6rDeFJ4FKPUufy') + }) +}) describe('Keychain - single privateKey', function suite() { this.timeout(10000); it('should correctly errors out when not a HDPublicKey (privateKey)', () => { @@ -108,18 +163,16 @@ describe('Keychain - single privateKey', function suite() { const network = 'livenet'; const pkKeyChain = new KeyChain({ privateKey, network }); expect(pkKeyChain.network).to.equal(network); - expect(pkKeyChain.keys).to.deep.equal({}); - expect(pkKeyChain.type).to.equal('privateKey'); - expect(pkKeyChain.privateKey).to.equal(privateKey); + expect(pkKeyChain.rootKeyType).to.equal('privateKey'); + expect(pkKeyChain.rootKey.toString()).to.equal(privateKey); - const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'; - const expectedException2 = 'Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate child'; - expect(() => pkKeyChain.generateKeyForPath()).to.throw(expectedException1); - expect(() => pkKeyChain.generateKeyForChild()).to.throw(expectedException2); + const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPrivateKey, impossible to derivate keys for path m/0'; + expect(() => pkKeyChain.getForPath('m/0')).to.throw(expectedException1); }); it('should get private key', () => { const privateKey = Dashcore.PrivateKey().toString(); const pkKeyChain = new KeyChain({ privateKey, network: 'livenet' }); - expect(pkKeyChain.getPrivateKey().toString()).to.equal(privateKey); + expect(pkKeyChain.getRootKey().toString()).to.equal(privateKey); + expect(pkKeyChain.rootKey.toString()).to.equal(privateKey); }); }); diff --git a/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js b/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js deleted file mode 100644 index 5d6dcaeba0f..00000000000 --- a/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js +++ /dev/null @@ -1,19 +0,0 @@ -const { - HDPublicKey, -} = require('@dashevo/dashcore-lib'); -/** - * Derive from HDPrivateKey to a child - * @param {number} index - Child index to derivee to - * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys - * @return {HDPrivateKey|HDPublicKey} - */ -function generateKeyForChild(index, type = 'HDPrivateKey') { - if (!['HDPrivateKey', 'HDPublicKey'].includes(this.type)) { - throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate child'); - } - const HDKey = this[this.type]; - const hdPublicKey = HDKey.deriveChild(index); - if (type === 'HDPublicKey') return HDPublicKey(hdPublicKey); - return hdPublicKey; -} -module.exports = generateKeyForChild; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js b/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js deleted file mode 100644 index e266c96d757..00000000000 --- a/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js +++ /dev/null @@ -1,19 +0,0 @@ -const { - HDPublicKey, -} = require('@dashevo/dashcore-lib'); -/** - * Derive from HDPrivateKey to a specific path - * @param {string} path - * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys - * @return {HDPrivateKey|HDPublicKey} - */ -function generateKeyForPath(path, type = 'HDPrivateKey') { - if (!['HDPrivateKey', 'HDPublicKey'].includes(this.type)) { - throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'); - } - const HDKey = this[this.type]; - const hdPrivateKey = HDKey.derive(path); - if (type === 'HDPublicKey') return HDPublicKey(hdPrivateKey); - return hdPrivateKey; -} -module.exports = generateKeyForPath; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js index a7eed1547dc..b65dc10d7da 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js +++ b/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js @@ -8,7 +8,7 @@ * @return {HDPrivateKey|HDPublicKey} */ function getDIP15ExtendedKey(userUniqueId, contactUniqueId, index = 0, accountIndex = 0, type = 'HDPrivateKey') { - if (!['HDPrivateKey', 'HDPublicKey'].includes(this.type)) { + if (!['HDPrivateKey', 'HDPublicKey'].includes(this.rootKeyType)) { throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'); } if (!userUniqueId || !contactUniqueId) throw new Error('Required userUniqueId and contactUniqueId to be defined'); diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js b/packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js new file mode 100644 index 00000000000..cb72d60c493 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js @@ -0,0 +1,14 @@ +function getFirstUnusedAddress() { + const allUnused = this.getIssuedPaths() + .filter((path)=>{ + return path.isUsed === false + }); + + const firstUnused = allUnused.slice(0,1)[0] + + return { + path: firstUnused.path, + address: firstUnused.address.toString() + } +} +module.exports = getFirstUnusedAddress; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getForAddress.js b/packages/wallet-lib/src/types/KeyChain/methods/getForAddress.js new file mode 100644 index 00000000000..4b8a01ddf04 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getForAddress.js @@ -0,0 +1,12 @@ +function getForAddress(address) { + const searchResult = [...this.issuedPaths.entries()] + .find(([, el]) => el.address.toString() === address.toString()); + + if (!searchResult) { + return null; + } + const [path] = searchResult; + return this.getForPath(path); +} + +module.exports = getForAddress; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getForPath.js b/packages/wallet-lib/src/types/KeyChain/methods/getForPath.js new file mode 100644 index 00000000000..1388abdf05f --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getForPath.js @@ -0,0 +1,42 @@ +const logger = require('../../../logger'); + +function getForPath(path, opts = {}) { + if (path === undefined) throw new Error('Expect a valid path to derivate'); + const stringifiedPath = path.toString(); + logger.silly(`KeyChain.getForPath(${stringifiedPath})`); + const isUsed = (opts && opts.isUsed !== undefined) ? opts.isUsed : false; + const isWatched = (opts && opts.isWatched !== undefined) ? opts.isWatched : false; + const isDerivable = ['HDPrivateKey', 'HDPublicKey'].includes(this.rootKeyType); + if (!isDerivable && stringifiedPath !== '0') { + throw new Error(`Wallet is not loaded from a mnemonic or a HDPrivateKey, impossible to derivate keys for path ${stringifiedPath}`); + } + + let data; + if (this.issuedPaths.has(stringifiedPath)) { + data = this.issuedPaths.get(stringifiedPath); + if (opts && opts.isWatched !== undefined && data.isWatched !== opts.isWatched) { + data.isWatched = opts.isWatched; + } + if (opts && opts.isUsed !== undefined && data.isUsed !== opts.isUsed) { + data.isUsed = opts.isUsed; + } + return data; + } + + const key = (isDerivable) ? this.rootKey.derive(stringifiedPath) : this.getRootKey(); + + data = { + path: stringifiedPath, + key, + isUsed, + isWatched, + address: key.publicKey.toAddress(this.network), + issuedTime: +new Date(), + }; + + this.issuedPaths.set(stringifiedPath, data); + + return data; +} + +module.exports = getForPath; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js index 75a6628475f..48da61c1616 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js +++ b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js @@ -2,11 +2,10 @@ const { BIP44_TESTNET_ROOT_PATH, BIP44_LIVENET_ROOT_PATH } = require('../../../C /** * Return a safier root keys to derivate from - * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys * @return {HDPrivateKey|HDPublicKey} */ -function getHardenedBIP44HDKey(type = 'HDPrivateKey') { +function getHardenedBIP44HDKey() { const pathRoot = (this.network.toString() === 'testnet') ? BIP44_TESTNET_ROOT_PATH : BIP44_LIVENET_ROOT_PATH; - return this.generateKeyForPath(pathRoot, type); + return this.getForPath(pathRoot).key; } module.exports = getHardenedBIP44HDKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js index 06a4733afb0..422c9fab667 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js +++ b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js @@ -2,11 +2,10 @@ const { DIP9_LIVENET_ROOT_PATH, DIP9_TESTNET_ROOT_PATH } = require('../../../CON /** * Return a safier root path to derivate from - * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys * @return {HDPrivateKey|HDPublicKey} */ -function getHardenedDIP9FeatureHDKey(type = 'HDPrivateKey') { +function getHardenedDIP9FeatureHDKey() { const pathRoot = (this.network.toString() === 'testnet') ? DIP9_TESTNET_ROOT_PATH : DIP9_LIVENET_ROOT_PATH; - return this.generateKeyForPath(pathRoot, type); + return this.getForPath(pathRoot).key; } module.exports = getHardenedDIP9FeatureHDKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getIssuedPaths.js b/packages/wallet-lib/src/types/KeyChain/methods/getIssuedPaths.js new file mode 100644 index 00000000000..324750853ba --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getIssuedPaths.js @@ -0,0 +1,5 @@ +function getWatchedAddresses() { + return [...this.issuedPaths.values()]; +} + +module.exports = getWatchedAddresses; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js b/packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js deleted file mode 100644 index 768f1740d55..00000000000 --- a/packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generate a key by deriving it's direct child - * @param index - {Number} - * @return {HDPrivateKey | HDPublicKey} - */ -function getKeyForChild(index = 0, type = 'HDPrivateKey') { - return this.generateKeyForChild(index, type); -} - -module.exports = getKeyForChild; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js b/packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js deleted file mode 100644 index 4f7bb180d1d..00000000000 --- a/packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js +++ /dev/null @@ -1,29 +0,0 @@ -const { HDPrivateKey, PrivateKey } = require('@dashevo/dashcore-lib'); -/** - * Get a key from the cache or generate if none - * @param path - * @param type - def : HDPrivateKey - Expected return datatype of the keys - * @return {HDPrivateKey | HDPublicKey} - */ -function getKeyForPath(path, type = 'HDPrivateKey') { - if (type === 'HDPublicKey') { - // In this case, we do not generate or keep in cache. - return this.generateKeyForPath(path, type); - } - - if (this.type === 'HDPrivateKey') { - if (!this.keys[path]) { - this.keys[path] = this.generateKeyForPath(path, type).toString(); - } - return new HDPrivateKey(this.keys[path]); - } - if (this.type === 'privateKey') { - if (!this.keys[path]) { - this.keys[path] = this.getPrivateKey(path).toString(); - return new PrivateKey(this.keys[path]); - } - return new PrivateKey(this.keys[path]); - } - return new HDPrivateKey(this.keys[path]); -} -module.exports = getKeyForPath; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js deleted file mode 100644 index 602c287daf7..00000000000 --- a/packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js +++ /dev/null @@ -1,18 +0,0 @@ -const { - PrivateKey, -} = require('@dashevo/dashcore-lib'); - -/** - * @return {PrivateKey} - */ -function getPrivateKey() { - let pk; - if (this.type === 'HDPrivateKey') { - pk = PrivateKey(this.HDPrivateKey.privateKey); - } - if (this.type === 'privateKey') { - pk = PrivateKey(this.privateKey); - } - return pk; -} -module.exports = getPrivateKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getRootKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getRootKey.js new file mode 100644 index 00000000000..f2b54723a48 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getRootKey.js @@ -0,0 +1,5 @@ +function getRootKey() { + return this.rootKey; +} + +module.exports = getRootKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getWatchedAddresses.js b/packages/wallet-lib/src/types/KeyChain/methods/getWatchedAddresses.js new file mode 100644 index 00000000000..8154329119a --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getWatchedAddresses.js @@ -0,0 +1,7 @@ +function getWatchedAddresses() { + return [...this.issuedPaths.entries()] + .filter(([, el]) => el.isWatched === true) + .map(([, el]) => el.address.toString()); +} + +module.exports = getWatchedAddresses; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js b/packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js new file mode 100644 index 00000000000..9df149d7dd4 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js @@ -0,0 +1,18 @@ +const logger = require('../../../logger'); + +function markAddressAsUsed(address) { + + const searchResult = [...this.issuedPaths.entries()] + .find(([, el]) => el.address.toString() === address.toString()); + + if (searchResult) { + + const [, addressData] = searchResult; + logger.silly(`KeyChain - Marking ${address} ${addressData.path} as used`); + addressData.isUsed = true; + + return this.maybeLookAhead(); + } + +} +module.exports = markAddressAsUsed; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js b/packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js new file mode 100644 index 00000000000..e84db9fd842 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js @@ -0,0 +1,84 @@ +function maybeLookAhead() { + const { lookAheadOpts } = this; + const generatedPaths = []; + + if (Object.keys(lookAheadOpts.paths).length === 0) { + return generatedPaths; + } + + const usedPaths = [...this.issuedPaths.entries()] + .filter(([, el]) => el.isUsed === true) + .map(([path]) => path); + + const sortedUsedPathByBase = {}; + + usedPaths + .forEach((usedPath) => { + const splitted = usedPath.split('/'); + // Removes the index to sort which and how many base path has been generated + const basePath = splitted.splice(0, splitted.length - 1).join('/'); + if (!sortedUsedPathByBase[basePath]) sortedUsedPathByBase[basePath] = []; + sortedUsedPathByBase[basePath].push(usedPath); + }); + + const lastUsedIndexes = {}; + const lastGeneratedIndexes = {}; + + Object + .entries(lookAheadOpts.paths) + .forEach(([basePath]) => { + lastUsedIndexes[basePath] = -1; + lastGeneratedIndexes[basePath] = -1; + }); + + Object + .entries(sortedUsedPathByBase) + .forEach(([basePath, basePaths]) => { + // Sorting by index is also needed as the user might have manually issue a key + // and set it up to watched or used outside of lookAhead bounds + const sortedBasePaths = basePaths.sort((a, b) => a.split('/').splice(-1) - b.split('/').splice(-1)); + + let prevIndex; + sortedBasePaths.forEach((path) => { + const addressData = this.issuedPaths.get(path); + + const currentIndex = parseInt(path.split('/').splice(-1), 10); + + if (addressData.isUsed) { + lastUsedIndexes[basePath] = currentIndex; + } + + lastGeneratedIndexes[basePath] = currentIndex; + prevIndex = currentIndex; + }); + }); + + const isWatched = lookAheadOpts.isWatched || false; + + Object + .entries(lastGeneratedIndexes) + .forEach(([basePath]) => { + const lastUsedAndLastGenGap = lastGeneratedIndexes[basePath] - lastUsedIndexes[basePath]; + const pathAmountToGenerate = lookAheadOpts.paths[basePath] - lastUsedAndLastGenGap; + + if (pathAmountToGenerate > 0) { + const lastIndex = lastGeneratedIndexes[basePath]; + const lastIndexToGenerate = lastIndex + pathAmountToGenerate; + + if (lastIndexToGenerate > lastIndex) { + for ( + let index = lastIndex + 1; + index <= lastIndexToGenerate; + index += 1) { + const timeNow = +new Date(); + const pathData = this.getForPath(`${basePath}/${index}`, { isWatched }); + if (pathData.issuedTime >= timeNow) { + generatedPaths.push(pathData); + } + } + } + } + }); + return generatedPaths; +} +module.exports = maybeLookAhead; diff --git a/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.js b/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.js new file mode 100644 index 00000000000..fd5c24dfacb --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.js @@ -0,0 +1,16 @@ +class KeyChainStore { + constructor() { + this.keyChains = new Map(); + this.walletKeyChainId = null; + this.masterKeyChainId = null; + this.accountKeyChains = new Map(); + } +} + +KeyChainStore.prototype.addKeyChain = require('./methods/addKeyChain'); +KeyChainStore.prototype.getKeyChain = require('./methods/getKeyChain'); +KeyChainStore.prototype.getKeyChains = require('./methods/getKeyChains'); +KeyChainStore.prototype.makeChildKeyChainStore = require('./methods/makeChildKeyChainStore'); +KeyChainStore.prototype.getMasterKeyChain = require('./methods/getMasterKeyChain'); + +module.exports = KeyChainStore; diff --git a/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js b/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js new file mode 100644 index 00000000000..b32737bb7a2 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js @@ -0,0 +1,47 @@ +const {HDPrivateKey} = require("@dashevo/dashcore-lib"); +const KeyChainsStore = require('./KeyChainStore'); +const KeyChain = require("../KeyChain/KeyChain"); +const { expect } = require('chai'); + +describe('KeyChainStore', function suite() { + let keyChainsStore; + let hdPrivateKey = new HDPrivateKey() + let hdPublicKey = new HDPrivateKey().hdPublicKey + let keyChain = new KeyChain({HDPrivateKey: hdPrivateKey}) + let keyChainPublic = new KeyChain({HDPublicKey: hdPublicKey}) + let walletKeyChain = new KeyChain({HDPrivateKey:new HDPrivateKey()}); + it('should create a KeyChainStore', () => { + keyChainsStore = new KeyChainsStore(); + expect(keyChainsStore).to.exist; + expect(keyChainsStore.keyChains).to.be.a('Map') + }); + it('should be able to add a keyChain', function () { + keyChainsStore.addKeyChain(keyChain) + expect(keyChainsStore.keyChains.has(keyChain.keyChainId)).to.equal(true); + keyChainsStore.addKeyChain(keyChainPublic) + expect(keyChainsStore.keyChains.has(keyChainPublic.keyChainId)).to.equal(true); + }); + it('should allow to specify a specific master keychain', function () { + keyChainsStore.addKeyChain(walletKeyChain, { isMasterKeyChain: true }); + expect(keyChainsStore.keyChains.has(walletKeyChain.keyChainId)).to.equal(true); + }); + it('should get all keyChains', function () { + const keyChains = keyChainsStore.getKeyChains() + expect(keyChains).to.deep.equal([keyChain, keyChainPublic, walletKeyChain]); + }); + it('should get a keychain by its ID', () => { + const requestedKeychain = keyChainsStore.getKeyChain(keyChainPublic.keyChainId); + expect(requestedKeychain).to.equal(keyChainPublic); + }) + it('should get a master keychain', function () { + const requestedWalletKeyChain = keyChainsStore.getMasterKeyChain(); + expect(requestedWalletKeyChain).to.equal(walletKeyChain); + }); + it('should make a child key chain store', function () { + const childKeyChainStore = keyChainsStore.makeChildKeyChainStore('m/0') + expect(childKeyChainStore).to.exist; + expect(childKeyChainStore.keyChains).to.be.a('Map') + expect(childKeyChainStore.getMasterKeyChain().rootKeyType).to.be.equal(HDPrivateKey.name) + }); +}); + diff --git a/packages/wallet-lib/src/types/KeyChainStore/methods/addKeyChain.js b/packages/wallet-lib/src/types/KeyChainStore/methods/addKeyChain.js new file mode 100644 index 00000000000..04e60a50739 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/methods/addKeyChain.js @@ -0,0 +1,15 @@ +function addKeyChain(keychain, opts = {}) { + if (this.keyChains.has(keychain.keyChainId)) { + throw new Error(`Trying to add already existing keyChain ${keychain.keyChainId}`); + } + + this.keyChains.set(keychain.keyChainId, keychain); + + if (opts) { + if (opts.isMasterKeyChain && !this.masterKeyChainId) { + this.masterKeyChainId = keychain.keyChainId; + } + } +} + +module.exports = addKeyChain; diff --git a/packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChain.js b/packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChain.js new file mode 100644 index 00000000000..d964ba395ee --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChain.js @@ -0,0 +1,5 @@ +function getKeyChain(keyChainId) { + return this.keyChains.get(keyChainId); +} + +module.exports = getKeyChain; diff --git a/packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChains.js b/packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChains.js new file mode 100644 index 00000000000..4e0e16160cd --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/methods/getKeyChains.js @@ -0,0 +1,5 @@ +function getKeyChains() { + return Array.from(this.keyChains.values()); +} + +module.exports = getKeyChains; diff --git a/packages/wallet-lib/src/types/KeyChainStore/methods/getMasterKeyChain.js b/packages/wallet-lib/src/types/KeyChainStore/methods/getMasterKeyChain.js new file mode 100644 index 00000000000..512d6a28133 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/methods/getMasterKeyChain.js @@ -0,0 +1,6 @@ +function getMasterKeyChain() { + const keyChainId = this.masterKeyChainId; + return this.keyChains.get(keyChainId); +} + +module.exports = getMasterKeyChain; diff --git a/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js b/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js new file mode 100644 index 00000000000..b341184f90d --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js @@ -0,0 +1,21 @@ +const { Networks } = require('@dashevo/dashcore-lib'); +const { BIP44_LIVENET_ROOT_PATH, BIP44_TESTNET_ROOT_PATH } = require('../../../CONSTANTS'); +const KeyChain = require('../../KeyChain/KeyChain'); +const logger = require('../../../logger'); + +function makeChildKeyChainStore(path, opts) { + logger.debug(`KeyChainStore - make a child keychainstore for ${path}`); + const masterKeyChain = this.getMasterKeyChain(); + if (!masterKeyChain) throw new Error('Requires a master keychain to be added first.'); + + const childKeyChainStore = new this.constructor(); + const keyChainOpts = { network: masterKeyChain.network, ...opts }; + + // Accessing the type from getKeyForPath would behave on browser differently due to mangling. + keyChainOpts[masterKeyChain.rootKeyType] = masterKeyChain.getForPath(path).key; + const childKeyChain = new KeyChain(keyChainOpts); + childKeyChainStore.addKeyChain(childKeyChain, { isMasterKeyChain: true }); + return childKeyChainStore; +} + +module.exports = makeChildKeyChainStore; diff --git a/packages/wallet-lib/src/types/Wallet/Wallet.spec.js b/packages/wallet-lib/src/types/Wallet/Wallet.spec.js index 5f4a4368e20..a6d416bfbee 100644 --- a/packages/wallet-lib/src/types/Wallet/Wallet.spec.js +++ b/packages/wallet-lib/src/types/Wallet/Wallet.spec.js @@ -23,7 +23,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); - expect(wallet1.keyChain.type).to.be.deep.equal('HDPrivateKey'); + expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPrivateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -50,7 +50,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChain.type).to.be.deep.equal('HDPrivateKey'); + expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPrivateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -77,7 +77,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChain.type).to.be.deep.equal('HDPrivateKey'); + expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPrivateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -95,7 +95,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChain.type).to.be.deep.equal('HDPublicKey'); + expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPublicKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -112,7 +112,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChain.type).to.be.deep.equal('privateKey'); + expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('privateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -132,7 +132,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChain.type).to.be.deep.equal('publicKey'); + expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('publicKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -149,7 +149,7 @@ describe('Wallet - class', function suite() { expect(wallet2.plugins).to.be.deep.equal({}); expect(wallet2.accounts).to.be.deep.equal([]); expect(wallet2.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet2.keyChain.type).to.be.deep.equal('publicKey'); + expect(wallet2.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('publicKey'); expect(wallet2.passphrase).to.be.deep.equal(null); expect(wallet2.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet2.injectDefaultPlugins).to.be.deep.equal(true); @@ -170,7 +170,7 @@ describe('Wallet - class', function suite() { }); }); describe('Wallet - Get/Create Account', function suite() { - this.timeout(15000); + this.timeout(10000); const wallet1 = new Wallet({ mnemonic: fluidMnemonic.mnemonic, ...mocks }); it('should be able to create/get a wallet', async () => { @@ -246,6 +246,5 @@ describe('Wallet - Get/Create Account', function suite() { it('should not leak', () => { const mockOpts1 = { }; fromHDPublicKey.call(mockOpts1, gatherSail.testnet.external.hdpubkey); - expect(mockOpts1.keyChain.keys).to.deep.equal({}); }); }); diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js index c69c7749b9d..2397a91aab9 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js @@ -1,14 +1,18 @@ const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); +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(); - this.keyChain = new KeyChain({ address }); + + const keyChain = new KeyChain({ address, network }); + this.keyChainStore = new KeyChainStore(); + this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js index 44bd996a4d2..f0839308fc9 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js @@ -18,18 +18,20 @@ describe('Wallet - fromAddress', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.ADDRESS); expect(self1.mnemonic).to.equal(null); expect(self1.address).to.equal(cR4t6ePublicKey.toAddress().toString()); - expect(self1.keyChain.type).to.equal('address'); - expect(self1.keyChain.address).to.equal(cR4t6ePublicKey.toAddress().toString()); - expect(self1.keyChain.keys).to.deep.equal({}); + + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('address'); + expect(keyChain.rootKey).to.equal(cR4t6ePublicKey.toAddress().toString()); const self2 = {}; fromAddress.call(self2, cR4t6ePublicKey.toAddress().toString()); expect(self2.walletType).to.equal(WALLET_TYPES.ADDRESS); expect(self2.mnemonic).to.equal(null); expect(self2.address).to.equal(cR4t6ePublicKey.toAddress().toString()); - expect(self2.keyChain.type).to.equal('address'); - expect(self2.keyChain.address).to.equal(cR4t6ePublicKey.toAddress().toString()); - expect(self2.keyChain.keys).to.deep.equal({}); + + const keyChain2 = self2.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('address'); + expect(keyChain.rootKey).to.equal(cR4t6ePublicKey.toAddress().toString()); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js index da2aba6e047..90b6f1a9061 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js @@ -3,6 +3,7 @@ const { is, } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); +const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); const { WALLET_TYPES } = require('../../../CONSTANTS'); /** @@ -14,5 +15,8 @@ module.exports = function fromHDPrivateKey(hdPrivateKey) { this.walletType = WALLET_TYPES.HDWALLET; this.mnemonic = null; this.HDPrivateKey = HDPrivateKey(hdPrivateKey); - this.keyChain = new KeyChain({ HDPrivateKey: hdPrivateKey }); + + const keyChain = new KeyChain({ HDPrivateKey: this.HDPrivateKey }); + this.keyChainStore = new KeyChainStore(); + this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js index 5c277483190..c947a076b4c 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js @@ -16,9 +16,10 @@ describe('Wallet - fromHDPrivateKey', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(null); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - expect(self1.keyChain.type).to.equal('HDPrivateKey'); - expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - expect(self1.keyChain.keys).to.deep.equal({}); + + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js index 0631c051151..aa6a84078a1 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js @@ -2,6 +2,7 @@ const Dashcore = require('@dashevo/dashcore-lib'); const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); +const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); const normalizeHDPubKey = (key) => (is.string(key) ? Dashcore.HDPublicKey(key) : key); /** @@ -13,5 +14,8 @@ module.exports = function fromHDPublicKey(_hdPublicKey) { this.walletType = WALLET_TYPES.HDPUBLIC; this.mnemonic = null; this.HDPublicKey = normalizeHDPubKey(_hdPublicKey); - this.keyChain = new KeyChain({ HDPublicKey: this.HDPublicKey }); + + const keyChain = new KeyChain({ HDPublicKey: this.HDPublicKey }); + this.keyChainStore = new KeyChainStore(); + this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js index 96a1c9cf847..fca3387d34e 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js @@ -30,9 +30,10 @@ describe('Wallet - HDPublicKey', function suite() { expect(mockOpts1.mnemonic).to.equal(null); expect(mockOpts1.HDPublicKey.toString()).to.equal(gatherTestnet.external.hdpubkey); expect(new Dashcore.HDPublicKey(mockOpts1.HDPublicKey)).to.equal(mockOpts1.HDPublicKey); - expect(mockOpts1.keyChain.type).to.equal('HDPublicKey'); - expect(mockOpts1.keyChain.HDPublicKey).to.deep.equal(Dashcore.HDPublicKey(gatherTestnet.external.hdpubkey)); - expect(mockOpts1.keyChain.keys).to.deep.equal({}); + + const keyChain = mockOpts1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('HDPublicKey'); + expect(keyChain.rootKey).to.deep.equal(Dashcore.HDPublicKey(gatherTestnet.external.hdpubkey)); }); it('should work from a HDPubKey', () => { const wallet1 = new Wallet( @@ -45,7 +46,9 @@ describe('Wallet - HDPublicKey', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChain.type).to.be.deep.equal('HDPublicKey'); + + const keyChain = wallet1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.be.deep.equal('HDPublicKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -54,22 +57,28 @@ describe('Wallet - HDPublicKey', function suite() { expect(wallet1.exportWallet()).to.be.equal(gatherTestnet.external.hdpubkey); - wallet1.getAccount().then((account)=>{ - const unusedAddress = account.getUnusedAddress(); - const expectedUnused = { - path: "m/44'/1'/0'/0/0", - index: 0, - address: 'yNJ3xxTXXBBf39VfMBbBuLH2k57uAwxBxj', - transactions: [], - balanceSat: 0, - unconfirmedBalanceSat: 0, - utxos: {}, - fetchedLast: 0, - used: false, - }; - expect(unusedAddress).to.deep.equal(expectedUnused); + // FIXME: it appears we had introduced a bug here, + // as it is not possible to have a HDPublicKey derivation with hardened + // Either our path is m/44/1/0/0/0 or it is m/0/0. + // We should clarify this before merging TODO + wallet1 + .getAccount() + .then((account)=>{ + const unusedAddress = account.getUnusedAddress(); + const expectedUnused = { + path: "m/0/0", + index: 0, + address: 'yNJ3xxTXXBBf39VfMBbBuLH2k57uAwxBxj', + transactions: [], + balanceSat: 0, + unconfirmedBalanceSat: 0, + utxos: {}, + fetchedLast: 0, + used: false, + }; + expect(unusedAddress).to.deep.equal(expectedUnused); - wallet1.disconnect(); - }) + wallet1.disconnect(); + }) }); }); diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js index 4bdeaefefa8..1e2cbdda7a3 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js @@ -3,19 +3,25 @@ const { is, } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); +const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); 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); - this.keyChain = new KeyChain({ HDPrivateKey: this.HDPrivateKey }); + // 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 }); + this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js index 14e81028618..85f498c6172 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js @@ -15,27 +15,29 @@ describe('Wallet - fromMnemonic', function suite() { const self1 = { network: 'livenet', }; - fromMnemonic.call(self1, knifeFixture.mnemonic); + fromMnemonic.call(self1, knifeFixture.mnemonic, 'livenet'); expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(knifeFixture.mnemonic); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); expect(new Dashcore.HDPrivateKey(self1.HDPrivateKey)).to.equal(self1.HDPrivateKey); - expect(self1.keyChain.type).to.equal('HDPrivateKey'); - expect(self1.keyChain.network.name).to.equal('livenet'); - expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - expect(self1.keyChain.keys).to.deep.equal({}); + + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain.network).to.equal('livenet'); + expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); const self2 = {}; fromMnemonic.call(self2, knifeFixture.mnemonic); expect(self2.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self2.mnemonic).to.equal(knifeFixture.mnemonic); - expect(self2.keyChain.network.name).to.equal('testnet'); + + const keyChain2 = self2.keyChainStore.getMasterKeyChain() + expect(keyChain2.network).to.equal('testnet'); expect(self2.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); expect(new Dashcore.HDPrivateKey(self2.HDPrivateKey)).to.equal(self2.HDPrivateKey); - expect(self2.keyChain.type).to.equal('HDPrivateKey'); - expect(self2.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); - expect(self2.keyChain.keys).to.deep.equal({}); + expect(keyChain2.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain2.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ @@ -55,38 +57,35 @@ describe('Wallet - fromMnemonic - with passphrase', function suite() { this.timeout(10000); it('should correctly works with passphrase', () => { const self1 = { - network: 'livenet', - passphrase: knifeFixture.passphrase, }; - fromMnemonic.call(self1, knifeFixture.mnemonic); + fromMnemonic.call(self1, knifeFixture.mnemonic, 'livenet', knifeFixture.passphrase); expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(knifeFixture.mnemonic); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyMainnet); expect(new Dashcore.HDPrivateKey(self1.HDPrivateKey)).to.equal(self1.HDPrivateKey); - expect(self1.keyChain.type).to.equal('HDPrivateKey'); - expect(self1.keyChain.network.name).to.equal('livenet'); - expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyMainnet); - expect(self1.keyChain.keys).to.deep.equal({}); + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain.network).to.equal('livenet'); + expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyMainnet); const path1 = 'm/44\'/5\'/0\'/0/0'; - const pubKey1 = self1.keyChain.getKeyForPath(path1).publicKey.toAddress(); + const pubKey1 = keyChain.getForPath(path1).key.publicKey.toAddress(); expect(new Dashcore.Address(pubKey1).toString()).to.equal('Xq3zjky18WjwAHpLgGLasvX5g8TeLRKaxt'); const self2 = { - passphrase: knifeFixture.passphrase, }; - fromMnemonic.call(self2, knifeFixture.mnemonic); + fromMnemonic.call(self2, knifeFixture.mnemonic, 'testnet', knifeFixture.passphrase); expect(self2.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self2.mnemonic).to.equal(knifeFixture.mnemonic); expect(self2.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyTestnet); expect(new Dashcore.HDPrivateKey(self2.HDPrivateKey)).to.equal(self2.HDPrivateKey); - expect(self2.keyChain.type).to.equal('HDPrivateKey'); - expect(self2.keyChain.network.name).to.equal('testnet'); - expect(self2.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyTestnet); - expect(self2.keyChain.keys).to.deep.equal({}); + const keyChain2 = self2.keyChainStore.getMasterKeyChain() + expect(keyChain2.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain2.network).to.equal('testnet'); + expect(keyChain2.rootKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyTestnet); const path2 = 'm/44\'/1\'/0\'/0/0'; - const pubKey2 = self2.keyChain.getKeyForPath(path2).publicKey.toAddress(); + const pubKey2 = keyChain2.getForPath(path2).key.publicKey.toAddress(); expect(new Dashcore.Address(pubKey2, 'testnet').toString()).to.equal('yWYCH9XDRnpdNxh67jQJFkovToBVwWr8Ck'); }); }); diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js index 2f8f919bc7e..870af9937d8 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js @@ -1,15 +1,19 @@ const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); +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; - this.keyChain = new KeyChain({ privateKey }); + + const keyChain = new KeyChain({ privateKey, network }); + this.keyChainStore = new KeyChainStore(); + this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js index c707c2faddc..4b6ca5e89ab 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js @@ -16,18 +16,18 @@ describe('Wallet - fromPrivateKey', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.PRIVATEKEY); expect(self1.mnemonic).to.equal(null); expect(self1.privateKey).to.equal(cR4t6eFixture.privateKey); - expect(self1.keyChain.type).to.equal('privateKey'); - expect(self1.keyChain.privateKey).to.equal(cR4t6eFixture.privateKey); - expect(self1.keyChain.keys).to.deep.equal({}); + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('privateKey'); + expect(keyChain.rootKey.toWIF()).to.equal(cR4t6eFixture.privateKey); const self2 = {}; fromPrivateKey.call(self2, cR4t6eFixture.privateKey); expect(self2.walletType).to.equal(WALLET_TYPES.PRIVATEKEY); expect(self2.mnemonic).to.equal(null); expect(self2.privateKey).to.equal(cR4t6eFixture.privateKey); - expect(self2.keyChain.type).to.equal('privateKey'); - expect(self2.keyChain.privateKey).to.equal(cR4t6eFixture.privateKey); - expect(self2.keyChain.keys).to.deep.equal({}); + const keyChain2 = self2.keyChainStore.getMasterKeyChain() + expect(keyChain2.rootKeyType).to.equal('privateKey'); + expect(keyChain2.rootKey.toWIF()).to.equal(cR4t6eFixture.privateKey); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js index 9e6867ac78f..ffbf41344c9 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js @@ -1,15 +1,19 @@ const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); +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; - this.keyChain = new KeyChain({ publicKey }); + + const keyChain = new KeyChain({ publicKey, network }); + this.keyChainStore = new KeyChainStore(); + this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js index 8852ee6c6ee..07890f2d45d 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js @@ -18,18 +18,18 @@ describe('Wallet - fromPublicKey', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.PUBLICKEY); expect(self1.mnemonic).to.equal(null); expect(self1.publicKey).to.equal(cR4t6ePublicKey); - expect(self1.keyChain.type).to.equal('publicKey'); - expect(self1.keyChain.publicKey).to.equal(cR4t6ePublicKey); - expect(self1.keyChain.keys).to.deep.equal({}); + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('publicKey'); + expect(keyChain.rootKey.toString()).to.equal(cR4t6ePublicKey.toString()); const self2 = {}; fromPublicKey.call(self2, cR4t6ePublicKey.toString()); expect(self2.walletType).to.equal(WALLET_TYPES.PUBLICKEY); expect(self2.mnemonic).to.equal(null); expect(self2.publicKey).to.equal(cR4t6ePublicKey.toString()); - expect(self2.keyChain.type).to.equal('publicKey'); - expect(self2.keyChain.publicKey).to.equal(cR4t6ePublicKey.toString()); - expect(self2.keyChain.keys).to.deep.equal({}); + const keyChain2 = self2.keyChainStore.getMasterKeyChain() + expect(keyChain2.rootKeyType).to.equal('publicKey'); + expect(keyChain2.rootKey.toString()).to.equal(cR4t6ePublicKey.toString()); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.js b/packages/wallet-lib/src/types/Wallet/methods/fromSeed.js index 978adc9ecda..038fc138e6b 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.js +++ b/packages/wallet-lib/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/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js index 75e9d32cda1..a0a9ec827ca 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js @@ -19,22 +19,22 @@ describe('Wallet - fromSeed', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(null); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); - expect(self1.keyChain.type).to.equal('HDPrivateKey'); - expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); - expect(self1.keyChain.keys).to.deep.equal({}); + const keyChain = self1.keyChainStore.getMasterKeyChain() + expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); const self2 = { fromHDPrivateKey, network: 'mainnet', }; - fromSeed.call(self2, knifeFixture.seed); + fromSeed.call(self2, knifeFixture.seed, self2.network); expect(self2.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self2.mnemonic).to.equal(null); expect(self2.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - expect(self2.keyChain.type).to.equal('HDPrivateKey'); - expect(self2.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - expect(self2.keyChain.keys).to.deep.equal({}); + const keyChain2 = self2.keyChainStore.getMasterKeyChain() + expect(keyChain2.rootKeyType).to.equal('HDPrivateKey'); + expect(keyChain2.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js b/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js index 318d7959f8f..a4c84cbc784 100644 --- a/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js +++ b/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js @@ -1,5 +1,5 @@ const logger = require('../../logger'); -const { BIP44_ADDRESS_GAP } = require('../../CONSTANTS'); +const { BIP44_ADDRESS_GAP, WALLET_TYPES } = require('../../CONSTANTS'); const is = require('../is'); const getMissingIndexes = require('./getMissingIndexes'); @@ -77,13 +77,15 @@ function ensureAccountAddressesToGapLimit(walletStore, walletType, accountIndex, const gapBetweenLastUsedAndLastGenerated = { external: lastGeneratedIndexes.external - lastUsedIndexes.external, - internal: lastGeneratedIndexes.internal - lastUsedIndexes.internal, }; const addressesToGenerate = { external: BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.external, - internal: BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.internal, }; - + if (walletType.includes(WALLET_TYPES.HDWALLET)) { + // eslint-disable-next-line max-len + gapBetweenLastUsedAndLastGenerated.internal = lastGeneratedIndexes.internal - lastUsedIndexes.internal; + addressesToGenerate.internal = BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.internal; + } Object.entries(addressesToGenerate) .forEach(([typeToGenerate, numberToGenerate]) => { if (numberToGenerate > 0) { From 0078cd6d820f950cfd2e6c4170f7abdafe2b2441 Mon Sep 17 00:00:00 2001 From: Alex Werner Date: Mon, 31 Jan 2022 01:21:55 +0100 Subject: [PATCH 2/3] feat: rename KeyChain to DerivableKeyChain and reinstate old KeyChain --- .../src/types/Account/methods/encrypt.spec.js | 2 +- .../types/Account/methods/generateAddress.js | 72 ++----- .../src/types/Account/methods/getAddress.js | 46 +++-- .../DerivableKeyChain/DerivableKeyChain.d.ts | 50 +++++ .../DerivableKeyChain/DerivableKeyChain.js | 107 +++++++++++ .../DerivableKeyChain.spec.js | 178 ++++++++++++++++++ .../methods/getDIP15ExtendedKey.js | 26 +++ .../methods/getFirstUnusedAddress.js | 12 ++ .../methods/getForAddress.js | 0 .../methods/getForPath.js | 0 .../methods/getHardenedBIP44HDKey.js | 11 ++ .../methods/getHardenedDIP15AccountKey.js | 14 ++ .../methods/getHardenedDIP9FeatureHDKey.js | 11 ++ .../methods/getIssuedPaths.js | 0 .../methods/getRootKey.js | 0 .../methods/getWatchedAddresses.js | 0 .../methods/markAddressAsUsed.js | 3 +- .../methods/maybeLookAhead.js | 2 - .../types/DerivableKeyChain/methods/sign.js | 29 +++ .../src/types/Identities/Identities.js | 2 +- .../methods/getIdentityHDKeyById.js | 5 +- .../methods/getIdentityHDKeyById.spec.js | 8 +- .../src/types/KeyChain/KeyChain.d.ts | 62 +++--- .../wallet-lib/src/types/KeyChain/KeyChain.js | 131 ++++--------- .../src/types/KeyChain/KeyChain.spec.js | 121 ++++-------- .../KeyChain/methods/generateKeyForChild.js | 19 ++ .../KeyChain/methods/generateKeyForPath.js | 19 ++ .../KeyChain/methods/getDIP15ExtendedKey.js | 2 +- .../KeyChain/methods/getFirstUnusedAddress.js | 14 -- .../KeyChain/methods/getHardenedBIP44HDKey.js | 5 +- .../methods/getHardenedDIP9FeatureHDKey.js | 5 +- .../types/KeyChain/methods/getKeyForChild.js | 10 + .../types/KeyChain/methods/getKeyForPath.js | 29 +++ .../types/KeyChain/methods/getPrivateKey.js | 18 ++ .../types/KeyChainStore/KeyChainStore.spec.js | 8 +- .../methods/makeChildKeyChainStore.js | 6 +- .../src/types/Wallet/Wallet.spec.js | 17 +- .../src/types/Wallet/methods/fromAddress.js | 8 +- .../types/Wallet/methods/fromAddress.spec.js | 14 +- .../types/Wallet/methods/fromHDPrivateKey.js | 6 +- .../Wallet/methods/fromHDPrivateKey.spec.js | 7 +- .../types/Wallet/methods/fromHDPublicKey.js | 6 +- .../Wallet/methods/fromHDPublicKey.spec.js | 49 ++--- .../src/types/Wallet/methods/fromMnemonic.js | 14 +- .../types/Wallet/methods/fromMnemonic.spec.js | 47 ++--- .../types/Wallet/methods/fromPrivateKey.js | 8 +- .../Wallet/methods/fromPrivateKey.spec.js | 12 +- .../src/types/Wallet/methods/fromPublicKey.js | 8 +- .../Wallet/methods/fromPublicKey.spec.js | 12 +- .../src/types/Wallet/methods/fromSeed.js | 4 +- .../src/types/Wallet/methods/fromSeed.spec.js | 14 +- .../utils/bip44/ensureAddressesToGapLimit.js | 10 +- 52 files changed, 802 insertions(+), 461 deletions(-) create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.d.ts create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.js create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/methods/getDIP15ExtendedKey.js create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/methods/getFirstUnusedAddress.js rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/getForAddress.js (100%) rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/getForPath.js (100%) create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedBIP44HDKey.js create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP15AccountKey.js create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP9FeatureHDKey.js rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/getIssuedPaths.js (100%) rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/getRootKey.js (100%) rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/getWatchedAddresses.js (100%) rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/markAddressAsUsed.js (96%) rename packages/wallet-lib/src/types/{KeyChain => DerivableKeyChain}/methods/maybeLookAhead.js (97%) create mode 100644 packages/wallet-lib/src/types/DerivableKeyChain/methods/sign.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js delete mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js create mode 100644 packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js diff --git a/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js b/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js index facc317ea96..89653f84bdc 100644 --- a/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js +++ b/packages/wallet-lib/src/types/Account/methods/encrypt.spec.js @@ -29,7 +29,7 @@ describe('Account - encrypt', function suite() { const secret = 'secret'; it('should encrypt extPubKey with aes', () => { - const extPubKey = account.keyChainStore.getMasterKeyChain().getForPath(derivationPath).key.toString(); + const extPubKey = account.keyChain.getKeyForPath(derivationPath, 'HDPublicKey').toString(); const encryptedExtPubKey = account.encrypt('aes', extPubKey, secret).toString(); const bytes = CryptoJS.AES.decrypt(encryptedExtPubKey, secret); const decrypted = bytes.toString(CryptoJS.enc.Utf8); diff --git a/packages/wallet-lib/src/types/Account/methods/generateAddress.js b/packages/wallet-lib/src/types/Account/methods/generateAddress.js index 6d19109c108..450f582fe73 100644 --- a/packages/wallet-lib/src/types/Account/methods/generateAddress.js +++ b/packages/wallet-lib/src/types/Account/methods/generateAddress.js @@ -1,94 +1,58 @@ +const { PublicKey } = require('@dashevo/dashcore-lib'); const EVENTS = require('../../../EVENTS'); const { WALLET_TYPES } = require('../../../CONSTANTS'); 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 {AddressInfo} Address information + * @return {AddressObj} Address information * */ -function generateAddress(path, isWatchedAddress = true) { +function generateAddress(path) { if (is.undefOrNull(path)) throw new Error('Expected path to generate an address'); let index = 0; + let privateKey; let address; - let keyPathData; const { network } = this; switch (this.walletType) { case WALLET_TYPES.ADDRESS: - address = this.keyChainStore.getMasterKeyChain().rootKey; - if (isWatchedAddress) { - this.keyChainStore.issuedPaths.set(0, { - path: 0, - address, - isUsed: false, - isWatched: true, - }); - } + address = this.keyChain.address; break; case WALLET_TYPES.PUBLICKEY: - // eslint-disable-next-line no-case-declarations - const { rootKey } = this.keyChainStore.getMasterKeyChain(); - address = rootKey.toAddress(network).toString(); - if (isWatchedAddress) { - this.keyChainStore.issuedPaths.set(0, { - key: rootKey, - path: 0, - address, - isUsed: false, - isWatched: true, - }); - } + address = new PublicKey(this.keyChain.publicKey.toString()).toAddress(network).toString(); break; - case WALLET_TYPES.HDPRIVATE: case WALLET_TYPES.HDWALLET: // eslint-disable-next-line prefer-destructuring - index = parseInt(path.toString().split('/')[2], 10); - keyPathData = this.keyChainStore - .getMasterKeyChain() - .getForPath(path, { isWatched: isWatchedAddress }); - address = keyPathData.address.toString(); + index = parseInt(path.toString().split('/')[5], 10); + privateKey = this.keyChain.getKeyForPath(path); + address = privateKey.publicKey.toAddress(network).toString(); break; case WALLET_TYPES.HDPUBLIC: index = parseInt(path.toString().split('/')[5], 10); - // eslint-disable-next-line no-case-declarations - keyPathData = this.keyChainStore - .getMasterKeyChain() - .getForPath(path, { isWatched: isWatchedAddress }); - address = keyPathData.address.toString(); + privateKey = this.keyChain.getKeyForChild(index); + address = privateKey.publicKey.toAddress(network).toString(); break; - // TODO: DEPRECATE USAGE OF SINGLE_ADDRESS in favor or PRIVATEKEY - case WALLET_TYPES.PRIVATEKEY: case WALLET_TYPES.SINGLE_ADDRESS: default: - keyPathData = this.keyChainStore - .getMasterKeyChain() - .getForPath(path, { isWatched: isWatchedAddress }); - address = keyPathData.address.toString(); - break; + privateKey = this.keyChain.getKeyForPath(path.toString()); + address = privateKey.publicKey.toAddress(network).toString(); } const addressData = { path: path.toString(), index, address, + // privateKey, transactions: [], - utxos: {}, balanceSat: 0, unconfirmedBalanceSat: 0, + utxos: {}, + fetchedLast: 0, + used: false, }; - 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.storage.importAddresses(addressData, this.walletId); this.emit(EVENTS.GENERATED_ADDRESS, { type: EVENTS.GENERATED_ADDRESS, payload: addressData }); return addressData; } - module.exports = generateAddress; diff --git a/packages/wallet-lib/src/types/Account/methods/getAddress.js b/packages/wallet-lib/src/types/Account/methods/getAddress.js index 86efad07386..d4771dc7738 100644 --- a/packages/wallet-lib/src/types/Account/methods/getAddress.js +++ b/packages/wallet-lib/src/types/Account/methods/getAddress.js @@ -1,26 +1,40 @@ const { WALLET_TYPES } = require('../../../CONSTANTS'); +const getTypePathFromWalletType = (walletType, addressType = 'external', index, BIP44PATH) => { + let type; + let path; + + const addressTypeIndex = (addressType === 'external') ? 0 : 1; + switch (walletType) { + case WALLET_TYPES.HDWALLET: + type = addressType; + path = `${BIP44PATH}/${addressTypeIndex}/${index}`; + break; + case WALLET_TYPES.HDPUBLIC: + type = 'external'; + path = `${BIP44PATH}/${addressTypeIndex}/${index}`; + 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} [addressType="external"] - Type of the address (external, internal, misc) + * @param {AddressType} [_type="external"] - Type of the address (external, internal, misc) * @return */ -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); +function getAddress(index = 0, _type = 'external') { + const { type, path } = getTypePathFromWalletType(this.walletType, _type, index, this.BIP44PATH); - const chainStore = this.storage.getChainStore(this.network); - return { - index: addressIndex, - path: addressPath, - ...chainStore.getAddress(address), - }; + const { wallets } = this.storage.getStore(); + const matchingTypeAddresses = wallets[this.walletId].addresses[type]; + return (matchingTypeAddresses[path]) ? matchingTypeAddresses[path] : this.generateAddress(path); } module.exports = getAddress; diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.d.ts b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.d.ts new file mode 100644 index 00000000000..406d2baf5ff --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.d.ts @@ -0,0 +1,50 @@ +import {PrivateKey, Network,} from "../types"; +import {HDPrivateKey, HDPublicKey} from "@dashevo/dashcore-lib"; +import {Transaction} from "@dashevo/dashcore-lib/typings/transaction/Transaction"; + +export declare namespace DerivableKeyChain { + interface IDerivableKeyChainOptions { + network?: Network; + keys?: [Keys] + } +} + +export declare class DerivableKeyChain { + constructor(options?: DerivableKeyChain.IDerivableKeyChainOptions); + network: Network; + keys: [Keys]; + + type: HDKeyTypesParam|PrivateKeyTypeParam; + HDPrivateKey?: HDPrivateKey; + privateKey?: PrivateKey; + + generateKeyForChild(index: number, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey; + generateKeyForPath(path: string, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey; + + getDIP15ExtendedKey(userUniqueId: string, contactUniqueId: string, index?: number, accountIndex?: number, type?: HDKeyTypesParam): HDKeyTypes; + getHardenedDIP15AccountKey(index?: number, type?: HDKeyTypesParam): HDKeyTypes; + getHardenedBIP44HDKey(type?: HDKeyTypesParam): HDKeyTypes; + getHardenedDIP9FeatureHDKey(type?: HDKeyTypesParam): HDKeyTypes; + getKeyForChild(index: number, type?: HDKeyTypesParam): HDKeyTypes; + getKeyForPath(path: string, type?: HDKeyTypesParam): HDKeyTypes; + getPrivateKey(): PrivateKey; + + sign(object: Transaction|any, privateKeys:[PrivateKey], sigType: number): any; +} + +type HDKeyTypes = HDPublicKey | HDPrivateKey; + +export declare enum HDKeyTypesParam { + HDPrivateKey="HDPrivateKey", + HDPublicKey="HDPrivateKey", +} +export declare enum PrivateKeyTypeParam { + privateKey='privateKey' +} +export declare interface Keys { + [path: string]: { + path: string + }; +} + + diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.js b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.js new file mode 100644 index 00000000000..9191f40d7e2 --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.js @@ -0,0 +1,107 @@ +const { Networks, HDPrivateKey, HDPublicKey } = require('@dashevo/dashcore-lib'); +const { PrivateKey, PublicKey } = require('@dashevo/dashcore-lib'); +const { doubleSha256 } = require('../../utils/crypto'); +const { mnemonicToHDPrivateKey } = require('../../utils/mnemonic'); + +function generateKeyChainId(key) { + const keyChainIdSuffix = doubleSha256(key.toString()).toString('hex').slice(0, 10); + return `kc${keyChainIdSuffix}`; +} + +function fromOptions(opts) { + let rootKey; + let rootKeyType; + let network = Networks.testnet.toString(); + let passphrase = ''; + + if (opts) { + if (opts.passphrase) { + passphrase = opts.passphrase; + } + if (opts.mnemonic) { + rootKeyType = 'HDPrivateKey'; + rootKey = (typeof opts.mnemonic === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; + } + if (opts.network) { + network = opts.network; + } + if (opts.HDPrivateKey) { + rootKeyType = 'HDPrivateKey'; + rootKey = (typeof opts.HDPrivateKey === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; + network = rootKey.network.toString(); + } else if (opts.HDPublicKey) { + rootKeyType = 'HDPublicKey'; + rootKey = (typeof opts.HDPublicKey === 'string') ? HDPublicKey(opts.HDPublicKey) : opts.HDPublicKey; + network = rootKey.network.toString(); + } else if (opts.privateKey) { + rootKeyType = 'privateKey'; + rootKey = (typeof opts.privateKey === 'string') ? new PrivateKey(opts.privateKey, opts.network) : opts.privateKey; + network = rootKey.network.toString(); + } else if (opts.publicKey) { + rootKeyType = 'publicKey'; + rootKey = (typeof opts.publicKey === 'string') ? new PublicKey(opts.publicKey, opts.network) : opts.publicKey; + network = rootKey.network.toString(); + } else if (opts.address) { + rootKeyType = 'address'; + rootKey = opts.address.toString(); + } else if (opts.mnemonic) { + return fromOptions({ + ...opts, + HDPrivateKey: mnemonicToHDPrivateKey(opts.mnemonic, network, passphrase), + }); + } + } + + const lookAheadOpts = { + isWatched: true, + paths: {}, + ...opts.lookAheadOpts, + }; + + return { + rootKeyType, + rootKey, + network, + passphrase, + lookAheadOpts, + }; +} + +class DerivableKeyChain { + constructor(opts = {}) { + const { + rootKey, + rootKeyType, + network, + lookAheadOpts, + } = fromOptions(opts); + if (!rootKeyType || !rootKey) { + throw new Error('Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.'); + } + this.keyChainId = generateKeyChainId(rootKey); + + this.rootKey = rootKey; + this.network = network; + this.rootKeyType = rootKeyType; + this.lookAheadOpts = { isWatched: true, ...lookAheadOpts }; + + this.issuedPaths = new Map(); + + this.maybeLookAhead(); + } +} +DerivableKeyChain.prototype.getForPath = require('./methods/getForPath'); +DerivableKeyChain.prototype.getForAddress = require('./methods/getForAddress'); +DerivableKeyChain.prototype.getDIP15ExtendedKey = require('./methods/getDIP15ExtendedKey'); +DerivableKeyChain.prototype.getFirstUnusedAddress = require('./methods/getFirstUnusedAddress'); +DerivableKeyChain.prototype.getHardenedBIP44HDKey = require('./methods/getHardenedBIP44HDKey'); +DerivableKeyChain.prototype.getHardenedDIP9FeatureHDKey = require('./methods/getHardenedDIP9FeatureHDKey'); +DerivableKeyChain.prototype.getHardenedDIP15AccountKey = require('./methods/getHardenedDIP15AccountKey'); +DerivableKeyChain.prototype.getRootKey = require('./methods/getRootKey'); +DerivableKeyChain.prototype.getWatchedAddresses = require('./methods/getWatchedAddresses'); +DerivableKeyChain.prototype.getIssuedPaths = require('./methods/getIssuedPaths'); +DerivableKeyChain.prototype.maybeLookAhead = require('./methods/maybeLookAhead'); +DerivableKeyChain.prototype.markAddressAsUsed = require('./methods/markAddressAsUsed'); +DerivableKeyChain.prototype.sign = require('./methods/sign'); + +module.exports = DerivableKeyChain; diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js new file mode 100644 index 00000000000..98dfc69bb5b --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js @@ -0,0 +1,178 @@ +const Dashcore = require('@dashevo/dashcore-lib'); +const { expect } = require('chai'); +const DerivableKeyChain = require('./DerivableKeyChain'); +const { mnemonicToHDPrivateKey } = require('../../utils/mnemonic'); + +let derivableKeyChain; +let derivableKeyChain2; +const mnemonic = 'during develop before curtain hazard rare job language become verb message travel'; +const mnemonic2 = 'birth kingdom trash renew flavor utility donkey gasp regular alert pave layer'; +const pk = '4226d5e2fe8cbfe6f5beb7adf5a5b08b310f6c4a67fc27826779073be6f5699e'; +const hdPublicKey = 'xpub661MyMwAqRbcFGB6XSWBsD725rJDUbFUpy4zWe2u22nJ2BxpoHFxtVDfKnTnvVQHohnY7AsVpRTHDv6PyPQTYu1KxFPKw29MAVXPEpz1G7V'; +const expectedRootDIP15AccountKey_0 = 'tprv8hRzmheQujhJN5XP2dj955nAFCKeEoSifJRWuutdbwWRtusdDQ426jbp75EqErUSuTxmPyxYmP1TpcF5qdxGhXLNXRLMGsRLG6NFCv1WnaQ'; +const expectedRootDIP15AccountKey_1 = 'tprv8hRzmheQujhJQyCtFTuUFHxB3Ag5VLB994zhH4CfxbA41cq73HT2mpYq5M33V54oJyn6g514saxxVJB886G55eYX56J6D6x87UNNT6iQHkR'; +const expectedKeyForChild_0 = 'tprv8d4podc2Tg459CH2bwLHXj3vdJFBT2rdsk5Nr1djH7hzHdt5LRdvN6QyFwMiDy7ffRdik7fEVRKKgsHB4F18sh8xF6jFXpKq4sUgGBoSbKw'; +describe('DerivableKeyChain', function suite() { + this.timeout(1000); + it('should create a DerivableKeyChain', () => { + const expectedException1 = 'Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.'; + expect(() => new DerivableKeyChain()).to.throw(expectedException1); + + derivableKeyChain = new DerivableKeyChain({ mnemonic: mnemonic, network: 'testnet' }); + expect(derivableKeyChain.rootKeyType).to.equal('HDPrivateKey'); + expect(derivableKeyChain.network.toString()).to.equal('testnet'); + expect(derivableKeyChain.rootKey.network.toString()).to.equal('testnet'); + + derivableKeyChain2 = new DerivableKeyChain({ mnemonic: mnemonic2, network: 'livenet' }); + }); + it('should generate key for full path', () => { + const path = 'm/44\'/1\'/0\'/0/0'; + const pk2 = derivableKeyChain.getForPath(path).key; + const address = new Dashcore.Address(pk2.publicKey.toAddress()).toString(); + expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT'); + }); + it('should get hardened feature path', () => { + const hardenedPk = derivableKeyChain.getHardenedBIP44HDKey(); + const pk2 = derivableKeyChain.getForPath('m/44\'/1\'').key; + expect(pk2.toString()).to.equal(hardenedPk.toString()); + }); + it('should get DIP15 account key', function () { + const rootDIP15AccountKey_0 = derivableKeyChain.getHardenedDIP15AccountKey(0); + expect(rootDIP15AccountKey_0.toString()).to.deep.equal(expectedRootDIP15AccountKey_0); + const rootDIP15AccountKey_1 = derivableKeyChain.getHardenedDIP15AccountKey(1); + expect(rootDIP15AccountKey_1.toString()).to.deep.equal(expectedRootDIP15AccountKey_1); + }); + + it('should get DIP15 extended key', function () { + const userUniqueId = '0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a'; + const contactUniqueId = '0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'; + + // m/9'/5'/15'/0'/0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a'/0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'/0 + const DIP15ExtPubKey_0 = derivableKeyChain2.getDIP15ExtendedKey(userUniqueId, contactUniqueId, 0, 0, type='HDPublicKey'); + expect(DIP15ExtPubKey_0.toString()).to.equal('xpub6LTkTQFSb8KMgMSz4B6sMZLpkQAY6wSTDprDkHDmLwWLpnjxazuxZn13FrSLKUafitsxuaaffM5a49P6aswhpppWUuYW6eFnwBXshR2W2eY'); + expect(DIP15ExtPubKey_0.publicKey.toString()).to.equal('038030c88ab0106e1f4af3b939db2bafc56f892554106f08da1ce1f9ef10f807bd') + + const DIP15ExtPrivKey_0 = derivableKeyChain2.getDIP15ExtendedKey(userUniqueId, contactUniqueId, 0, 0); + expect(DIP15ExtPrivKey_0.toString()).to.equal('xprvA7UQ3tiYkkm4TsNWx9ZrzRQ6CNL3hUibrbvcwtp9nbyMwzQp3Tbi1ygZQaPoigDhCf8XUjMmGK2NbnB2kLXPYg99Lp6e3iki318sdWcFN3q'); + expect(DIP15ExtPrivKey_0.privateKey.toString()).to.equal('fac40790776d171ee1db90899b5eb2df2f7d2aaf35ad56f07ffb8ed2c57f8e60') + expect(DIP15ExtPrivKey_0.publicKey.toString()).to.equal('038030c88ab0106e1f4af3b939db2bafc56f892554106f08da1ce1f9ef10f807bd') + + const userAhash = "0xa11ce14f698b32e9bb306dba7bbbee831263dcf658abeebb39930460ead117e5"; + const userBhash = "0xb0b052ff075c5ca3c16c3e20e9ac8223834475cc1324ab07889cb24ce6a62793"; + const DIP15ExtKey_1 = derivableKeyChain.getDIP15ExtendedKey(userAhash, userBhash, 0, 0); + expect(DIP15ExtKey_1.privateKey.toString()).to.equal('60581b6dca8244d3fb3cfe619b5a22277e5423b01e5285f356981f247e0f4a60') + expect(DIP15ExtKey_1.publicKey.toString()).to.equal('03deaac00f721151307fbc7bf80d7b8afab98c1f026d67e5f56b21e2013f551ce6') + }); + it('should derive from hardened feature path', () => { + const hardenedHDKey = derivableKeyChain.getHardenedBIP44HDKey(); + const pk2 = derivableKeyChain.getForPath(`m/44'/1'`).key; + expect(pk2.toString()).to.equal(hardenedHDKey.toString()); + expect(hardenedHDKey.toString()).to.deep.equal('tprv8dtrJNytYHRiZY585hmHGbguS6VjGpK49puSB7oXZjLHcQfrAzQkF4ZCxM2DkEbyY85J4EYcZ8EjT5ZCU8ozB727TDdodbfXet5GkGau2RQ'); + const derivedPk = hardenedHDKey.deriveChild(0, true).deriveChild(0).deriveChild(0); + const address = new Dashcore.Address(derivedPk.publicKey.toAddress()).toString(); + expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT'); + }); + it('should get hardened DIP9FeatureHDKey', function () { + const hardenedHDKey = derivableKeyChain.getHardenedDIP9FeatureHDKey(); + const pk2 = derivableKeyChain.getForPath(`m/9'/1'`).key; + expect(pk2.toString()).to.equal(hardenedHDKey.toString()); + expect(hardenedHDKey.toString()).to.deep.equal('tprv8fBJjWoGgCpGRCbyzE9RUA59rmoN1RUijhLnXGL4VHnLxvSe523yVg4GrGzbR6TyXtdynAEh5z8UX55EXt2Cb3xjvrsx2PgTY9BHxzFVkWn'); + }); + it('should get key for path', () => { + const derivableKeyChain2 = new DerivableKeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') }); + const keyForChild = derivableKeyChain2.getForPath('m/0').key; + expect(keyForChild.toString()).to.equal(expectedKeyForChild_0); + }); + + it('should mark address watched and get watched addresses', function () { + const key0 = derivableKeyChain.getForPath('m/0'); + derivableKeyChain.getForPath('m/0').isWatched = true + key0.isWatched = true; + const key1 = derivableKeyChain.getForPath('m/1', { isWatched: true }); + const key2 = derivableKeyChain.getForPath('m/2', { isWatched: true }); + + const watchedAddresses = derivableKeyChain.getWatchedAddresses(); + let expectedWatchedAddresses = [ + derivableKeyChain.getForPath('m/0').address.toString(), + derivableKeyChain.getForPath('m/1').address.toString(), + derivableKeyChain.getForPath('m/2').address.toString() + ]; + expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); + }); + it('should get watched addresses', function () { + const watchedAddresses = derivableKeyChain.getWatchedAddresses(); + const expectedWatchedAddresses = [ + 'ybQDfNwiDjk8ZH5UUmHQzAMEmjbrbK5dAj', + 'yhFX5rseJPitV45HUCaa9haeGHtLuooBaq', + 'yhqxsmYk6jfoGWf1hJKq7d4U2cGHCgzpFU' + ] + expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); + }); + // it('should get watched public keys', function () { + // const watchedPubKeys = derivableKeyChain.getWatchedPublicKeys(); + // const expectedWatchedPubKeys = [ + // '03e6ab8177a7ca2699da4f83ca3c27768fb88b70ae9d6bde1cba8de88355ccf199', + // '0246a870b65153b98e453ff08f7198d06bce2a790286f44d90929aceafafa0673f', + // '025ad16f78f67801e52abe5b512d66e3896d4c2fa3ca3150349437a5dd13519967' + // ] + // expect(watchedPubKeys).to.deep.equal(expectedWatchedPubKeys) + // }); + it('should remove an address from watched addresses', function () { + const data0 = derivableKeyChain.getForPath('m/0', { isWatched: false }); + const data1 = derivableKeyChain.getForPath('m/1'); + const data2 = derivableKeyChain.getForPath('m/2'); + data2.isWatched = false; + + expect(derivableKeyChain.getWatchedAddresses().length).to.equal(1); + }); + it('should get address for path', function (){ + const address0_1 = derivableKeyChain.getForPath('m/1').address; + expect(address0_1.toString()).to.equal('yhFX5rseJPitV45HUCaa9haeGHtLuooBaq') + }) + it('should mark address as used', function () { + const address0_0 = derivableKeyChain.getForPath('m/0').address; + derivableKeyChain.markAddressAsUsed(address0_0); + expect(derivableKeyChain.issuedPaths.get('m/0').isUsed).to.equal(true) + }); +}); +describe('DerivableKeyChain - HDPublicKey', function suite(){ + let hdpubDerivableKeyChain; + it('should initiate from a HDPublicKey', function () { + hdpubDerivableKeyChain = new DerivableKeyChain({ + HDPublicKey: new Dashcore.HDPublicKey(hdPublicKey), + network: 'testnet' + }); + // As the HDPublicKey starts with xpub, it's livenet and should take priority over our network being set. + expect(hdpubDerivableKeyChain.network.toString()).to.equal('livenet'); + expect(hdpubDerivableKeyChain.keyChainId).to.equal('kc5059442d66'); + expect(hdpubDerivableKeyChain.getRootKey().toString()).to.equal(hdPublicKey); + }); + it('should derivate', function () { + const key0_1 = hdpubDerivableKeyChain.getForPath('m/1').key; + expect(key0_1.publicKey.toAddress(hdpubDerivableKeyChain.network).toString()).to.equal('XoL5LcBiDWcj6L7fFwytsFoX5Vz7BVXw9w') + }); + it('should get address for path', function (){ + const address0_1 = hdpubDerivableKeyChain.getForPath('m/2').address; + expect(address0_1.toString()).to.equal('XwAzpxQKbgebaLiadq1c6rDeFJ4FKPUufy') + }) +}) +describe('DerivableKeyChain - single privateKey', function suite() { + this.timeout(10000); + it('should correctly errors out when not a HDPublicKey (privateKey)', () => { + const privateKey = Dashcore.PrivateKey().toString(); + const network = 'livenet'; + const pkDerivableKeyChain = new DerivableKeyChain({ privateKey, network }); + expect(pkDerivableKeyChain.network).to.equal(network); + expect(pkDerivableKeyChain.rootKeyType).to.equal('privateKey'); + expect(pkDerivableKeyChain.rootKey.toString()).to.equal(privateKey); + + const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPrivateKey, impossible to derivate keys for path m/0'; + expect(() => pkDerivableKeyChain.getForPath('m/0')).to.throw(expectedException1); + }); + it('should get private key', () => { + const privateKey = Dashcore.PrivateKey().toString(); + const pkDerivableKeyChain = new DerivableKeyChain({ privateKey, network: 'livenet' }); + expect(pkDerivableKeyChain.getRootKey().toString()).to.equal(privateKey); + expect(pkDerivableKeyChain.rootKey.toString()).to.equal(privateKey); + }); +}); diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/methods/getDIP15ExtendedKey.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getDIP15ExtendedKey.js new file mode 100644 index 00000000000..b65dc10d7da --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getDIP15ExtendedKey.js @@ -0,0 +1,26 @@ +/** + * Return the extended key of the relationship between two dashpay contacts. + * @param userUniqueId - Current userID + * @param contactUniqueId - Contact userID + * @param index - the key index. + * @param accountIndex[=0] - the internal wallet account from which derivation is done + * @param type {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys + * @return {HDPrivateKey|HDPublicKey} + */ +function getDIP15ExtendedKey(userUniqueId, contactUniqueId, index = 0, accountIndex = 0, type = 'HDPrivateKey') { + if (!['HDPrivateKey', 'HDPublicKey'].includes(this.rootKeyType)) { + throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'); + } + if (!userUniqueId || !contactUniqueId) throw new Error('Required userUniqueId and contactUniqueId to be defined'); + + // Require a HDPrivateKey for hardened derivation + const extendedPrivateKey = this + .getHardenedDIP15AccountKey(accountIndex, 'HDPrivateKey') + .deriveChild((userUniqueId), true) + .deriveChild((contactUniqueId), true) + .deriveChild(index, false); + + return (type === 'HDPublicKey' ? extendedPrivateKey.hdPublicKey : extendedPrivateKey); +} + +module.exports = getDIP15ExtendedKey; diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/methods/getFirstUnusedAddress.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getFirstUnusedAddress.js new file mode 100644 index 00000000000..ab7f3fefeeb --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getFirstUnusedAddress.js @@ -0,0 +1,12 @@ +function getFirstUnusedAddress() { + const allUnused = this.getIssuedPaths() + .filter((path) => path.isUsed === false); + + const firstUnused = allUnused.slice(0, 1)[0]; + + return { + path: firstUnused.path, + address: firstUnused.address.toString(), + }; +} +module.exports = getFirstUnusedAddress; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getForAddress.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getForAddress.js similarity index 100% rename from packages/wallet-lib/src/types/KeyChain/methods/getForAddress.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/getForAddress.js diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getForPath.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getForPath.js similarity index 100% rename from packages/wallet-lib/src/types/KeyChain/methods/getForPath.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/getForPath.js diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedBIP44HDKey.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedBIP44HDKey.js new file mode 100644 index 00000000000..48da61c1616 --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedBIP44HDKey.js @@ -0,0 +1,11 @@ +const { BIP44_TESTNET_ROOT_PATH, BIP44_LIVENET_ROOT_PATH } = require('../../../CONSTANTS'); + +/** + * Return a safier root keys to derivate from + * @return {HDPrivateKey|HDPublicKey} + */ +function getHardenedBIP44HDKey() { + const pathRoot = (this.network.toString() === 'testnet') ? BIP44_TESTNET_ROOT_PATH : BIP44_LIVENET_ROOT_PATH; + return this.getForPath(pathRoot).key; +} +module.exports = getHardenedBIP44HDKey; diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP15AccountKey.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP15AccountKey.js new file mode 100644 index 00000000000..c01f240fc85 --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP15AccountKey.js @@ -0,0 +1,14 @@ +/** + * Return a safier root path to derivate from + * @param {number} [accountIndex=0] - set the account index + * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys + * @return {HDPrivateKey|HDPublicKey} + */ +function getHardenedDIP15AccountKey(accountIndex = 0, type = 'HDPrivateKey') { + const hardenedFeatureRootKey = this.getHardenedDIP9FeatureHDKey(type); + + // Feature is set to 15' for all DashPay Incoming Funds derivation paths (see DIP15). + const featureKey = hardenedFeatureRootKey.deriveChild(15, true); + return featureKey.deriveChild(accountIndex, true); +} +module.exports = getHardenedDIP15AccountKey; diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP9FeatureHDKey.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP9FeatureHDKey.js new file mode 100644 index 00000000000..422c9fab667 --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getHardenedDIP9FeatureHDKey.js @@ -0,0 +1,11 @@ +const { DIP9_LIVENET_ROOT_PATH, DIP9_TESTNET_ROOT_PATH } = require('../../../CONSTANTS'); + +/** + * Return a safier root path to derivate from + * @return {HDPrivateKey|HDPublicKey} + */ +function getHardenedDIP9FeatureHDKey() { + const pathRoot = (this.network.toString() === 'testnet') ? DIP9_TESTNET_ROOT_PATH : DIP9_LIVENET_ROOT_PATH; + return this.getForPath(pathRoot).key; +} +module.exports = getHardenedDIP9FeatureHDKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getIssuedPaths.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getIssuedPaths.js similarity index 100% rename from packages/wallet-lib/src/types/KeyChain/methods/getIssuedPaths.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/getIssuedPaths.js diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getRootKey.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getRootKey.js similarity index 100% rename from packages/wallet-lib/src/types/KeyChain/methods/getRootKey.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/getRootKey.js diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getWatchedAddresses.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/getWatchedAddresses.js similarity index 100% rename from packages/wallet-lib/src/types/KeyChain/methods/getWatchedAddresses.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/getWatchedAddresses.js diff --git a/packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/markAddressAsUsed.js similarity index 96% rename from packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/markAddressAsUsed.js index 9df149d7dd4..cecf2922e1f 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/markAddressAsUsed.js +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/markAddressAsUsed.js @@ -1,12 +1,10 @@ const logger = require('../../../logger'); function markAddressAsUsed(address) { - const searchResult = [...this.issuedPaths.entries()] .find(([, el]) => el.address.toString() === address.toString()); if (searchResult) { - const [, addressData] = searchResult; logger.silly(`KeyChain - Marking ${address} ${addressData.path} as used`); addressData.isUsed = true; @@ -14,5 +12,6 @@ function markAddressAsUsed(address) { return this.maybeLookAhead(); } + return false; } module.exports = markAddressAsUsed; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/maybeLookAhead.js similarity index 97% rename from packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js rename to packages/wallet-lib/src/types/DerivableKeyChain/methods/maybeLookAhead.js index e84db9fd842..fd3841f40bd 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/maybeLookAhead.js +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/maybeLookAhead.js @@ -38,7 +38,6 @@ function maybeLookAhead() { // and set it up to watched or used outside of lookAhead bounds const sortedBasePaths = basePaths.sort((a, b) => a.split('/').splice(-1) - b.split('/').splice(-1)); - let prevIndex; sortedBasePaths.forEach((path) => { const addressData = this.issuedPaths.get(path); @@ -49,7 +48,6 @@ function maybeLookAhead() { } lastGeneratedIndexes[basePath] = currentIndex; - prevIndex = currentIndex; }); }); diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/methods/sign.js b/packages/wallet-lib/src/types/DerivableKeyChain/methods/sign.js new file mode 100644 index 00000000000..b10b561f2c2 --- /dev/null +++ b/packages/wallet-lib/src/types/DerivableKeyChain/methods/sign.js @@ -0,0 +1,29 @@ +const { + crypto, Transaction, Message, +} = require('@dashevo/dashcore-lib'); + +/** + * Allow to sign any transaction or a transition object from a valid privateKeys list + * @param {Transaction|any} object + * @param {[PrivateKey]} privateKeys + * @param {number} [sigType=crypto.Signature.SIGHASH_ALL] + */ +function sign(object, privateKeys, sigType = crypto.Signature.SIGHASH_ALL) { + const handledTypes = [Transaction.name, Transaction.Payload.SubTxRegisterPayload, Message.name]; + if (!privateKeys) throw new Error('Require one or multiple privateKeys to sign'); + if (!object) throw new Error('Nothing to sign'); + if (!handledTypes.includes(object.constructor.name)) { + throw new Error(`Keychain sign : Unhandled object of type ${object.constructor.name}`); + } + const obj = object.sign(privateKeys, sigType); + + if (obj.isFullySigned && !obj.isFullySigned()) { + throw new Error('Not fully signed transaction'); + } + if (object.constructor.name === 'Message') { + // When signed, message are in string form. + return Message(obj); + } + return obj; +} +module.exports = sign; diff --git a/packages/wallet-lib/src/types/Identities/Identities.js b/packages/wallet-lib/src/types/Identities/Identities.js index f457fe9fc99..f721356dcce 100644 --- a/packages/wallet-lib/src/types/Identities/Identities.js +++ b/packages/wallet-lib/src/types/Identities/Identities.js @@ -10,7 +10,7 @@ class Identities { this.storage = wallet.storage; - this.keyChain = wallet.keyChainStore.getMasterKeyChain(); + this.keyChain = wallet.keyChain; } } diff --git a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js index 79cd6eedc77..374925a14a3 100644 --- a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js +++ b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.js @@ -5,10 +5,7 @@ * @return {HDPrivateKey} */ function getIdentityHDKeyById(identityId, keyIndex) { - const identityIndex = this.storage - .getWalletStore(this.walletId) - .getIndexedIdentityIds() - .indexOf(identityId); + const identityIndex = this.storage.getIndexedIdentityIds(this.walletId).indexOf(identityId); if (identityIndex === -1) { throw new Error(`Identity with ID ${identityId} is not associated with wallet, or it's not synced`); diff --git a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js index 2d8f3a2b829..e13dc087569 100644 --- a/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js +++ b/packages/wallet-lib/src/types/Identities/methods/getIdentityHDKeyById.spec.js @@ -1,6 +1,7 @@ const { expect } = require('chai'); const mockedStore = require('../../../../fixtures/sirentonight-fullstore-snapshot-1562711703'); const getIdentityHDKeyById = require('./getIdentityHDKeyById'); +const searchTransaction = require('../../Storage/methods/searchTransaction'); let walletMock; let fetchTransactionInfoCalledNb = 0; @@ -9,15 +10,12 @@ describe('Wallet#getIdentityHDKeyById', function suite() { this.timeout(10000); before(() => { expectedKeyMock = "123"; - const walletStoreMock = { - getIndexedIdentityIds: () => mockedStore.wallets[Object.keys(mockedStore.wallets)].identityIds - } - const storageMock = { store: mockedStore, getStore: () => mockedStore, mappedAddress: {}, - getWalletStore: () => walletStoreMock, + searchTransaction, + getIndexedIdentityIds: () => mockedStore.wallets[Object.keys(mockedStore.wallets)].identityIds, }; const walletId = Object.keys(mockedStore.wallets)[0]; walletMock = { diff --git a/packages/wallet-lib/src/types/KeyChain/KeyChain.d.ts b/packages/wallet-lib/src/types/KeyChain/KeyChain.d.ts index 0c1ee65a322..0d774d60b6f 100644 --- a/packages/wallet-lib/src/types/KeyChain/KeyChain.d.ts +++ b/packages/wallet-lib/src/types/KeyChain/KeyChain.d.ts @@ -3,48 +3,46 @@ import {HDPrivateKey, HDPublicKey} from "@dashevo/dashcore-lib"; import {Transaction} from "@dashevo/dashcore-lib/typings/transaction/Transaction"; export declare namespace KeyChain { - interface IKeyChainOptions { - network?: Network; - keys?: [Keys] - } + interface IKeyChainOptions { + network?: Network; + keys?: [Keys] + } } export declare class KeyChain { - constructor(options?: KeyChain.IKeyChainOptions); - network: Network; - keys: [Keys]; - - type: HDKeyTypesParam|PrivateKeyTypeParam; - HDPrivateKey?: HDPrivateKey; - privateKey?: PrivateKey; - - generateKeyForChild(index: number, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey; - generateKeyForPath(path: string, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey; - - getDIP15ExtendedKey(userUniqueId: string, contactUniqueId: string, index?: number, accountIndex?: number, type?: HDKeyTypesParam): HDKeyTypes; - getHardenedDIP15AccountKey(index?: number, type?: HDKeyTypesParam): HDKeyTypes; - getHardenedBIP44HDKey(type?: HDKeyTypesParam): HDKeyTypes; - getHardenedDIP9FeatureHDKey(type?: HDKeyTypesParam): HDKeyTypes; - getKeyForChild(index: number, type?: HDKeyTypesParam): HDKeyTypes; - getKeyForPath(path: string, type?: HDKeyTypesParam): HDKeyTypes; - getPrivateKey(): PrivateKey; - - sign(object: Transaction|any, privateKeys:[PrivateKey], sigType: number): any; + constructor(options?: KeyChain.IKeyChainOptions); + network: Network; + keys: [Keys]; + + type: HDKeyTypesParam|PrivateKeyTypeParam; + HDPrivateKey?: HDPrivateKey; + privateKey?: PrivateKey; + + generateKeyForChild(index: number, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey; + generateKeyForPath(path: string, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey; + + getDIP15ExtendedKey(userUniqueId: string, contactUniqueId: string, index?: number, accountIndex?: number, type?: HDKeyTypesParam): HDKeyTypes; + getHardenedDIP15AccountKey(index?: number, type?: HDKeyTypesParam): HDKeyTypes; + getHardenedBIP44HDKey(type?: HDKeyTypesParam): HDKeyTypes; + getHardenedDIP9FeatureHDKey(type?: HDKeyTypesParam): HDKeyTypes; + getKeyForChild(index: number, type?: HDKeyTypesParam): HDKeyTypes; + getKeyForPath(path: string, type?: HDKeyTypesParam): HDKeyTypes; + getPrivateKey(): PrivateKey; + + sign(object: Transaction|any, privateKeys:[PrivateKey], sigType: number): any; } type HDKeyTypes = HDPublicKey | HDPrivateKey; export declare enum HDKeyTypesParam { - HDPrivateKey="HDPrivateKey", - HDPublicKey="HDPrivateKey", + HDPrivateKey="HDPrivateKey", + HDPublicKey="HDPrivateKey", } export declare enum PrivateKeyTypeParam { - privateKey='privateKey' + privateKey='privateKey' } export declare interface Keys { - [path: string]: { - path: string - }; + [path: string]: { + path: string + }; } - - diff --git a/packages/wallet-lib/src/types/KeyChain/KeyChain.js b/packages/wallet-lib/src/types/KeyChain/KeyChain.js index 74931778764..643dcc62737 100644 --- a/packages/wallet-lib/src/types/KeyChain/KeyChain.js +++ b/packages/wallet-lib/src/types/KeyChain/KeyChain.js @@ -1,107 +1,52 @@ const { Networks, HDPrivateKey, HDPublicKey } = require('@dashevo/dashcore-lib'); -const { PrivateKey, PublicKey } = require('@dashevo/dashcore-lib'); -const { doubleSha256 } = require('../../utils/crypto'); -const { mnemonicToHDPrivateKey } = require('../../utils/mnemonic'); +const { has } = require('lodash'); -function generateKeyChainId(key) { - const keyChainIdSuffix = doubleSha256(key.toString()).toString('hex').slice(0, 10); - return `kc${keyChainIdSuffix}`; -} - -function fromOptions(opts) { - let rootKey; - let rootKeyType; - let network = Networks.testnet.toString(); - let passphrase = ''; - - if (opts) { - if (opts.passphrase) { - passphrase = opts.passphrase; - } - if (opts.mnemonic) { - rootKeyType = 'HDPrivateKey'; - rootKey = (typeof opts.mnemonic === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; - } - if (opts.network) { - network = opts.network; - } - if (opts.HDPrivateKey) { - rootKeyType = 'HDPrivateKey'; - rootKey = (typeof opts.HDPrivateKey === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; - network = rootKey.network.toString(); - } else if (opts.HDPublicKey) { - rootKeyType = 'HDPublicKey'; - rootKey = (typeof opts.HDPublicKey === 'string') ? HDPublicKey(opts.HDPublicKey) : opts.HDPublicKey; - network = rootKey.network.toString(); - } else if (opts.privateKey) { - rootKeyType = 'privateKey'; - rootKey = (typeof opts.privateKey === 'string') ? new PrivateKey(opts.privateKey, opts.network) : opts.privateKey; - network = rootKey.network.toString(); - } else if (opts.publicKey) { - rootKeyType = 'publicKey'; - rootKey = (typeof opts.publicKey === 'string') ? new PublicKey(opts.publicKey, opts.network) : opts.publicKey; - network = rootKey.network.toString(); - } else if (opts.address) { - rootKeyType = 'address'; - rootKey = opts.address.toString(); - } else if (opts.mnemonic) { - return fromOptions({ - ...opts, - HDPrivateKey: mnemonicToHDPrivateKey(opts.mnemonic, network, passphrase), - }); - } - } - - const lookAheadOpts = { - isWatched: true, - paths: {}, - ...opts.lookAheadOpts, - }; - - return { - rootKeyType, - rootKey, - network, - passphrase, - lookAheadOpts, - }; -} +// eslint-disable-next-line no-underscore-dangle +const _defaultOpts = { + network: Networks.testnet.toString(), + keys: {}, +}; class KeyChain { - constructor(opts = {}) { - const { - rootKey, - rootKeyType, - network, - lookAheadOpts, - } = fromOptions(opts); - if (!rootKeyType || !rootKey) { - throw new Error('Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.'); + constructor(opts = JSON.parse(JSON.stringify(_defaultOpts))) { + const defaultOpts = JSON.parse(JSON.stringify(_defaultOpts)); + this.network = defaultOpts.network; + this.keys = { ...defaultOpts.keys }; + + if (has(opts, 'HDPrivateKey')) { + this.type = 'HDPrivateKey'; + this.HDPrivateKey = (typeof opts.HDPrivateKey === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey; + this.network = this.HDPrivateKey.network; + } else if (has(opts, 'HDPublicKey')) { + this.type = 'HDPublicKey'; + this.HDPublicKey = (typeof opts.HDPublicKey === 'string') ? HDPublicKey(opts.HDPublicKey) : opts.HDPublicKey; + this.network = this.HDPublicKey.network; + } else if (has(opts, 'privateKey')) { + this.type = 'privateKey'; + this.privateKey = opts.privateKey; + } else if (has(opts, 'publicKey')) { + this.type = 'publicKey'; + this.publicKey = opts.publicKey; + } else if (has(opts, 'address')) { + this.type = 'address'; + this.address = opts.address.toString(); + } else { + throw new Error('Expect privateKey, publicKey, HDPublicKey, HDPrivateKey or Address'); } - this.keyChainId = generateKeyChainId(rootKey); - - this.rootKey = rootKey; - this.network = network; - this.rootKeyType = rootKeyType; - this.lookAheadOpts = { isWatched: true, ...lookAheadOpts }; - - this.issuedPaths = new Map(); - - this.maybeLookAhead(); + if (opts.network) this.network = opts.network; + if (opts.keys) this.keys = { ...opts.keys }; } } -KeyChain.prototype.getForPath = require('./methods/getForPath'); -KeyChain.prototype.getForAddress = require('./methods/getForAddress'); + +KeyChain.prototype.generateKeyForChild = require('./methods/generateKeyForChild'); +KeyChain.prototype.generateKeyForPath = require('./methods/generateKeyForPath'); KeyChain.prototype.getDIP15ExtendedKey = require('./methods/getDIP15ExtendedKey'); -KeyChain.prototype.getFirstUnusedAddress = require('./methods/getFirstUnusedAddress'); KeyChain.prototype.getHardenedBIP44HDKey = require('./methods/getHardenedBIP44HDKey'); KeyChain.prototype.getHardenedDIP9FeatureHDKey = require('./methods/getHardenedDIP9FeatureHDKey'); KeyChain.prototype.getHardenedDIP15AccountKey = require('./methods/getHardenedDIP15AccountKey'); -KeyChain.prototype.getRootKey = require('./methods/getRootKey'); -KeyChain.prototype.getWatchedAddresses = require('./methods/getWatchedAddresses'); -KeyChain.prototype.getIssuedPaths = require('./methods/getIssuedPaths'); -KeyChain.prototype.maybeLookAhead = require('./methods/maybeLookAhead'); -KeyChain.prototype.markAddressAsUsed = require('./methods/markAddressAsUsed'); +KeyChain.prototype.getKeyForChild = require('./methods/getKeyForChild'); +KeyChain.prototype.getKeyForPath = require('./methods/getKeyForPath'); +KeyChain.prototype.getPrivateKey = require('./methods/getPrivateKey'); KeyChain.prototype.sign = require('./methods/sign'); module.exports = KeyChain; diff --git a/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js b/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js index f392769cdda..3cf9bb0b5a1 100644 --- a/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js +++ b/packages/wallet-lib/src/types/KeyChain/KeyChain.spec.js @@ -8,32 +8,36 @@ let keychain2; const mnemonic = 'during develop before curtain hazard rare job language become verb message travel'; const mnemonic2 = 'birth kingdom trash renew flavor utility donkey gasp regular alert pave layer'; const pk = '4226d5e2fe8cbfe6f5beb7adf5a5b08b310f6c4a67fc27826779073be6f5699e'; -const hdPublicKey = 'xpub661MyMwAqRbcFGB6XSWBsD725rJDUbFUpy4zWe2u22nJ2BxpoHFxtVDfKnTnvVQHohnY7AsVpRTHDv6PyPQTYu1KxFPKw29MAVXPEpz1G7V'; + const expectedRootDIP15AccountKey_0 = 'tprv8hRzmheQujhJN5XP2dj955nAFCKeEoSifJRWuutdbwWRtusdDQ426jbp75EqErUSuTxmPyxYmP1TpcF5qdxGhXLNXRLMGsRLG6NFCv1WnaQ'; const expectedRootDIP15AccountKey_1 = 'tprv8hRzmheQujhJQyCtFTuUFHxB3Ag5VLB994zhH4CfxbA41cq73HT2mpYq5M33V54oJyn6g514saxxVJB886G55eYX56J6D6x87UNNT6iQHkR'; const expectedKeyForChild_0 = 'tprv8d4podc2Tg459CH2bwLHXj3vdJFBT2rdsk5Nr1djH7hzHdt5LRdvN6QyFwMiDy7ffRdik7fEVRKKgsHB4F18sh8xF6jFXpKq4sUgGBoSbKw'; describe('Keychain', function suite() { - this.timeout(1000); + this.timeout(10000); it('should create a keychain', () => { - const expectedException1 = 'Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.'; + const expectedException1 = 'Expect privateKey, publicKey, HDPublicKey, HDPrivateKey or Address'; expect(() => new KeyChain()).to.throw(expectedException1); + expect(() => new KeyChain(mnemonic)).to.throw(expectedException1); - keychain = new KeyChain({ mnemonic: mnemonic, network: 'testnet' }); - expect(keychain.rootKeyType).to.equal('HDPrivateKey'); + keychain = new KeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') }); + expect(keychain.type).to.equal('HDPrivateKey'); expect(keychain.network.toString()).to.equal('testnet'); - expect(keychain.rootKey.network.toString()).to.equal('testnet'); + expect(keychain.keys).to.deep.equal({}); - keychain2 = new KeyChain({ mnemonic: mnemonic2, network: 'livenet' }); + keychain2 = new KeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic2, 'mainnet') }); + }); + it('should get private key', () => { + expect(keychain.getPrivateKey().toString()).to.equal(pk); }); it('should generate key for full path', () => { const path = 'm/44\'/1\'/0\'/0/0'; - const pk2 = keychain.getForPath(path).key; + const pk2 = keychain.getKeyForPath(path); const address = new Dashcore.Address(pk2.publicKey.toAddress()).toString(); expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT'); }); it('should get hardened feature path', () => { const hardenedPk = keychain.getHardenedBIP44HDKey(); - const pk2 = keychain.getForPath('m/44\'/1\'').key; + const pk2 = keychain.getKeyForPath('m/44\'/1\''); expect(pk2.toString()).to.equal(hardenedPk.toString()); }); it('should get DIP15 account key', function () { @@ -42,7 +46,6 @@ describe('Keychain', function suite() { const rootDIP15AccountKey_1 = keychain.getHardenedDIP15AccountKey(1); expect(rootDIP15AccountKey_1.toString()).to.deep.equal(expectedRootDIP15AccountKey_1); }); - it('should get DIP15 extended key', function () { const userUniqueId = '0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a'; const contactUniqueId = '0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'; @@ -62,10 +65,11 @@ describe('Keychain', function suite() { const DIP15ExtKey_1 = keychain.getDIP15ExtendedKey(userAhash, userBhash, 0, 0); expect(DIP15ExtKey_1.privateKey.toString()).to.equal('60581b6dca8244d3fb3cfe619b5a22277e5423b01e5285f356981f247e0f4a60') expect(DIP15ExtKey_1.publicKey.toString()).to.equal('03deaac00f721151307fbc7bf80d7b8afab98c1f026d67e5f56b21e2013f551ce6') + }); it('should derive from hardened feature path', () => { const hardenedHDKey = keychain.getHardenedBIP44HDKey(); - const pk2 = keychain.getForPath(`m/44'/1'`).key; + const pk2 = keychain.getKeyForPath(`m/44'/1'`); expect(pk2.toString()).to.equal(hardenedHDKey.toString()); expect(hardenedHDKey.toString()).to.deep.equal('tprv8dtrJNytYHRiZY585hmHGbguS6VjGpK49puSB7oXZjLHcQfrAzQkF4ZCxM2DkEbyY85J4EYcZ8EjT5ZCU8ozB727TDdodbfXet5GkGau2RQ'); const derivedPk = hardenedHDKey.deriveChild(0, true).deriveChild(0).deriveChild(0); @@ -74,88 +78,29 @@ describe('Keychain', function suite() { }); it('should get hardened DIP9FeatureHDKey', function () { const hardenedHDKey = keychain.getHardenedDIP9FeatureHDKey(); - const pk2 = keychain.getForPath(`m/9'/1'`).key; + const pk2 = keychain.getKeyForPath(`m/9'/1'`); expect(pk2.toString()).to.equal(hardenedHDKey.toString()); expect(hardenedHDKey.toString()).to.deep.equal('tprv8fBJjWoGgCpGRCbyzE9RUA59rmoN1RUijhLnXGL4VHnLxvSe523yVg4GrGzbR6TyXtdynAEh5z8UX55EXt2Cb3xjvrsx2PgTY9BHxzFVkWn'); }); - it('should get key for path', () => { + it('should generate key for child', () => { const keychain2 = new KeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') }); - const keyForChild = keychain2.getForPath('m/0').key; + const keyForChild = keychain2.generateKeyForChild(0); expect(keyForChild.toString()).to.equal(expectedKeyForChild_0); }); - it('should mark address watched and get watched addresses', function () { - const key0 = keychain.getForPath('m/0'); - keychain.getForPath('m/0').isWatched = true - key0.isWatched = true; - const key1 = keychain.getForPath('m/1', { isWatched: true }); - const key2 = keychain.getForPath('m/2', { isWatched: true }); - const watchedAddresses = keychain.getWatchedAddresses(); - let expectedWatchedAddresses = [ - keychain.getForPath('m/0').address.toString(), - keychain.getForPath('m/1').address.toString(), - keychain.getForPath('m/2').address.toString() - ]; - expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); - }); - it('should get watched addresses', function () { - const watchedAddresses = keychain.getWatchedAddresses(); - const expectedWatchedAddresses = [ - 'ybQDfNwiDjk8ZH5UUmHQzAMEmjbrbK5dAj', - 'yhFX5rseJPitV45HUCaa9haeGHtLuooBaq', - 'yhqxsmYk6jfoGWf1hJKq7d4U2cGHCgzpFU' - ] - expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); - }); - // it('should get watched public keys', function () { - // const watchedPubKeys = keychain.getWatchedPublicKeys(); - // const expectedWatchedPubKeys = [ - // '03e6ab8177a7ca2699da4f83ca3c27768fb88b70ae9d6bde1cba8de88355ccf199', - // '0246a870b65153b98e453ff08f7198d06bce2a790286f44d90929aceafafa0673f', - // '025ad16f78f67801e52abe5b512d66e3896d4c2fa3ca3150349437a5dd13519967' - // ] - // expect(watchedPubKeys).to.deep.equal(expectedWatchedPubKeys) - // }); - it('should remove an address from watched addresses', function () { - const data0 = keychain.getForPath('m/0', { isWatched: false }); - const data1 = keychain.getForPath('m/1'); - const data2 = keychain.getForPath('m/2'); - data2.isWatched = false; + it('should sign', () => { - expect(keychain.getWatchedAddresses().length).to.equal(1); - }); - it('should get address for path', function (){ - const address0_1 = keychain.getForPath('m/1').address; - expect(address0_1.toString()).to.equal('yhFX5rseJPitV45HUCaa9haeGHtLuooBaq') - }) - it('should mark address as used', function () { - const address0_0 = keychain.getForPath('m/0').address; - keychain.markAddressAsUsed(address0_0); - expect(keychain.issuedPaths.get('m/0').isUsed).to.equal(true) }); }); -describe('Keychain - HDPublicKey', function suite(){ - let hdpubKeyChain; - it('should initiate from a HDPublicKey', function () { - hdpubKeyChain = new KeyChain({ - HDPublicKey: new Dashcore.HDPublicKey(hdPublicKey), - network: 'testnet' - }); - // As the HDPublicKey starts with xpub, it's livenet and should take priority over our network being set. - expect(hdpubKeyChain.network.toString()).to.equal('livenet'); - expect(hdpubKeyChain.keyChainId).to.equal('kc5059442d66'); - expect(hdpubKeyChain.getRootKey().toString()).to.equal(hdPublicKey); - }); - it('should derivate', function () { - const key0_1 = hdpubKeyChain.getForPath('m/1').key; - expect(key0_1.publicKey.toAddress(hdpubKeyChain.network).toString()).to.equal('XoL5LcBiDWcj6L7fFwytsFoX5Vz7BVXw9w') +describe('Keychain - clone', function suite() { + this.timeout(10000); + it('should clone', () => { + const keychain2 = new KeyChain(keychain); + expect(keychain2).to.deep.equal(keychain); + expect(keychain2.keys).to.deep.equal(keychain.keys); }); - it('should get address for path', function (){ - const address0_1 = hdpubKeyChain.getForPath('m/2').address; - expect(address0_1.toString()).to.equal('XwAzpxQKbgebaLiadq1c6rDeFJ4FKPUufy') - }) -}) +}); describe('Keychain - single privateKey', function suite() { this.timeout(10000); it('should correctly errors out when not a HDPublicKey (privateKey)', () => { @@ -163,16 +108,18 @@ describe('Keychain - single privateKey', function suite() { const network = 'livenet'; const pkKeyChain = new KeyChain({ privateKey, network }); expect(pkKeyChain.network).to.equal(network); - expect(pkKeyChain.rootKeyType).to.equal('privateKey'); - expect(pkKeyChain.rootKey.toString()).to.equal(privateKey); + expect(pkKeyChain.keys).to.deep.equal({}); + expect(pkKeyChain.type).to.equal('privateKey'); + expect(pkKeyChain.privateKey).to.equal(privateKey); - const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPrivateKey, impossible to derivate keys for path m/0'; - expect(() => pkKeyChain.getForPath('m/0')).to.throw(expectedException1); + const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'; + const expectedException2 = 'Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate child'; + expect(() => pkKeyChain.generateKeyForPath()).to.throw(expectedException1); + expect(() => pkKeyChain.generateKeyForChild()).to.throw(expectedException2); }); it('should get private key', () => { const privateKey = Dashcore.PrivateKey().toString(); const pkKeyChain = new KeyChain({ privateKey, network: 'livenet' }); - expect(pkKeyChain.getRootKey().toString()).to.equal(privateKey); - expect(pkKeyChain.rootKey.toString()).to.equal(privateKey); + expect(pkKeyChain.getPrivateKey().toString()).to.equal(privateKey); }); }); diff --git a/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js b/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js new file mode 100644 index 00000000000..5d6dcaeba0f --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForChild.js @@ -0,0 +1,19 @@ +const { + HDPublicKey, +} = require('@dashevo/dashcore-lib'); +/** + * Derive from HDPrivateKey to a child + * @param {number} index - Child index to derivee to + * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys + * @return {HDPrivateKey|HDPublicKey} + */ +function generateKeyForChild(index, type = 'HDPrivateKey') { + if (!['HDPrivateKey', 'HDPublicKey'].includes(this.type)) { + throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate child'); + } + const HDKey = this[this.type]; + const hdPublicKey = HDKey.deriveChild(index); + if (type === 'HDPublicKey') return HDPublicKey(hdPublicKey); + return hdPublicKey; +} +module.exports = generateKeyForChild; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js b/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js new file mode 100644 index 00000000000..e266c96d757 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/generateKeyForPath.js @@ -0,0 +1,19 @@ +const { + HDPublicKey, +} = require('@dashevo/dashcore-lib'); +/** + * Derive from HDPrivateKey to a specific path + * @param {string} path + * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys + * @return {HDPrivateKey|HDPublicKey} + */ +function generateKeyForPath(path, type = 'HDPrivateKey') { + if (!['HDPrivateKey', 'HDPublicKey'].includes(this.type)) { + throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'); + } + const HDKey = this[this.type]; + const hdPrivateKey = HDKey.derive(path); + if (type === 'HDPublicKey') return HDPublicKey(hdPrivateKey); + return hdPrivateKey; +} +module.exports = generateKeyForPath; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js index b65dc10d7da..a7eed1547dc 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js +++ b/packages/wallet-lib/src/types/KeyChain/methods/getDIP15ExtendedKey.js @@ -8,7 +8,7 @@ * @return {HDPrivateKey|HDPublicKey} */ function getDIP15ExtendedKey(userUniqueId, contactUniqueId, index = 0, accountIndex = 0, type = 'HDPrivateKey') { - if (!['HDPrivateKey', 'HDPublicKey'].includes(this.rootKeyType)) { + if (!['HDPrivateKey', 'HDPublicKey'].includes(this.type)) { throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys'); } if (!userUniqueId || !contactUniqueId) throw new Error('Required userUniqueId and contactUniqueId to be defined'); diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js b/packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js deleted file mode 100644 index cb72d60c493..00000000000 --- a/packages/wallet-lib/src/types/KeyChain/methods/getFirstUnusedAddress.js +++ /dev/null @@ -1,14 +0,0 @@ -function getFirstUnusedAddress() { - const allUnused = this.getIssuedPaths() - .filter((path)=>{ - return path.isUsed === false - }); - - const firstUnused = allUnused.slice(0,1)[0] - - return { - path: firstUnused.path, - address: firstUnused.address.toString() - } -} -module.exports = getFirstUnusedAddress; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js index 48da61c1616..75a6628475f 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js +++ b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedBIP44HDKey.js @@ -2,10 +2,11 @@ const { BIP44_TESTNET_ROOT_PATH, BIP44_LIVENET_ROOT_PATH } = require('../../../C /** * Return a safier root keys to derivate from + * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys * @return {HDPrivateKey|HDPublicKey} */ -function getHardenedBIP44HDKey() { +function getHardenedBIP44HDKey(type = 'HDPrivateKey') { const pathRoot = (this.network.toString() === 'testnet') ? BIP44_TESTNET_ROOT_PATH : BIP44_LIVENET_ROOT_PATH; - return this.getForPath(pathRoot).key; + return this.generateKeyForPath(pathRoot, type); } module.exports = getHardenedBIP44HDKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js index 422c9fab667..06a4733afb0 100644 --- a/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js +++ b/packages/wallet-lib/src/types/KeyChain/methods/getHardenedDIP9FeatureHDKey.js @@ -2,10 +2,11 @@ const { DIP9_LIVENET_ROOT_PATH, DIP9_TESTNET_ROOT_PATH } = require('../../../CON /** * Return a safier root path to derivate from + * @param {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys * @return {HDPrivateKey|HDPublicKey} */ -function getHardenedDIP9FeatureHDKey() { +function getHardenedDIP9FeatureHDKey(type = 'HDPrivateKey') { const pathRoot = (this.network.toString() === 'testnet') ? DIP9_TESTNET_ROOT_PATH : DIP9_LIVENET_ROOT_PATH; - return this.getForPath(pathRoot).key; + return this.generateKeyForPath(pathRoot, type); } module.exports = getHardenedDIP9FeatureHDKey; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js b/packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js new file mode 100644 index 00000000000..768f1740d55 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getKeyForChild.js @@ -0,0 +1,10 @@ +/** + * Generate a key by deriving it's direct child + * @param index - {Number} + * @return {HDPrivateKey | HDPublicKey} + */ +function getKeyForChild(index = 0, type = 'HDPrivateKey') { + return this.generateKeyForChild(index, type); +} + +module.exports = getKeyForChild; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js b/packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js new file mode 100644 index 00000000000..4f7bb180d1d --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getKeyForPath.js @@ -0,0 +1,29 @@ +const { HDPrivateKey, PrivateKey } = require('@dashevo/dashcore-lib'); +/** + * Get a key from the cache or generate if none + * @param path + * @param type - def : HDPrivateKey - Expected return datatype of the keys + * @return {HDPrivateKey | HDPublicKey} + */ +function getKeyForPath(path, type = 'HDPrivateKey') { + if (type === 'HDPublicKey') { + // In this case, we do not generate or keep in cache. + return this.generateKeyForPath(path, type); + } + + if (this.type === 'HDPrivateKey') { + if (!this.keys[path]) { + this.keys[path] = this.generateKeyForPath(path, type).toString(); + } + return new HDPrivateKey(this.keys[path]); + } + if (this.type === 'privateKey') { + if (!this.keys[path]) { + this.keys[path] = this.getPrivateKey(path).toString(); + return new PrivateKey(this.keys[path]); + } + return new PrivateKey(this.keys[path]); + } + return new HDPrivateKey(this.keys[path]); +} +module.exports = getKeyForPath; diff --git a/packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js b/packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js new file mode 100644 index 00000000000..602c287daf7 --- /dev/null +++ b/packages/wallet-lib/src/types/KeyChain/methods/getPrivateKey.js @@ -0,0 +1,18 @@ +const { + PrivateKey, +} = require('@dashevo/dashcore-lib'); + +/** + * @return {PrivateKey} + */ +function getPrivateKey() { + let pk; + if (this.type === 'HDPrivateKey') { + pk = PrivateKey(this.HDPrivateKey.privateKey); + } + if (this.type === 'privateKey') { + pk = PrivateKey(this.privateKey); + } + return pk; +} +module.exports = getPrivateKey; diff --git a/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js b/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js index b32737bb7a2..bfb74a6cd81 100644 --- a/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js +++ b/packages/wallet-lib/src/types/KeyChainStore/KeyChainStore.spec.js @@ -1,15 +1,15 @@ const {HDPrivateKey} = require("@dashevo/dashcore-lib"); const KeyChainsStore = require('./KeyChainStore'); -const KeyChain = require("../KeyChain/KeyChain"); +const DerivableKeyChain = require("../DerivableKeyChain/DerivableKeyChain"); const { expect } = require('chai'); describe('KeyChainStore', function suite() { let keyChainsStore; let hdPrivateKey = new HDPrivateKey() let hdPublicKey = new HDPrivateKey().hdPublicKey - let keyChain = new KeyChain({HDPrivateKey: hdPrivateKey}) - let keyChainPublic = new KeyChain({HDPublicKey: hdPublicKey}) - let walletKeyChain = new KeyChain({HDPrivateKey:new HDPrivateKey()}); + let keyChain = new DerivableKeyChain({HDPrivateKey: hdPrivateKey}) + let keyChainPublic = new DerivableKeyChain({HDPublicKey: hdPublicKey}) + let walletKeyChain = new DerivableKeyChain({HDPrivateKey:new HDPrivateKey()}); it('should create a KeyChainStore', () => { keyChainsStore = new KeyChainsStore(); expect(keyChainsStore).to.exist; diff --git a/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js b/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js index b341184f90d..383baee3ce0 100644 --- a/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js +++ b/packages/wallet-lib/src/types/KeyChainStore/methods/makeChildKeyChainStore.js @@ -1,6 +1,4 @@ -const { Networks } = require('@dashevo/dashcore-lib'); -const { BIP44_LIVENET_ROOT_PATH, BIP44_TESTNET_ROOT_PATH } = require('../../../CONSTANTS'); -const KeyChain = require('../../KeyChain/KeyChain'); +const DerivableKeyChain = require('../../DerivableKeyChain/DerivableKeyChain'); const logger = require('../../../logger'); function makeChildKeyChainStore(path, opts) { @@ -13,7 +11,7 @@ function makeChildKeyChainStore(path, opts) { // Accessing the type from getKeyForPath would behave on browser differently due to mangling. keyChainOpts[masterKeyChain.rootKeyType] = masterKeyChain.getForPath(path).key; - const childKeyChain = new KeyChain(keyChainOpts); + const childKeyChain = new DerivableKeyChain(keyChainOpts); childKeyChainStore.addKeyChain(childKeyChain, { isMasterKeyChain: true }); return childKeyChainStore; } diff --git a/packages/wallet-lib/src/types/Wallet/Wallet.spec.js b/packages/wallet-lib/src/types/Wallet/Wallet.spec.js index a6d416bfbee..5f4a4368e20 100644 --- a/packages/wallet-lib/src/types/Wallet/Wallet.spec.js +++ b/packages/wallet-lib/src/types/Wallet/Wallet.spec.js @@ -23,7 +23,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); - expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPrivateKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('HDPrivateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -50,7 +50,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPrivateKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('HDPrivateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -77,7 +77,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPrivateKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('HDPrivateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -95,7 +95,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('HDPublicKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('HDPublicKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -112,7 +112,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('privateKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('privateKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -132,7 +132,7 @@ describe('Wallet - class', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet1.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('publicKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('publicKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -149,7 +149,7 @@ describe('Wallet - class', function suite() { expect(wallet2.plugins).to.be.deep.equal({}); expect(wallet2.accounts).to.be.deep.equal([]); expect(wallet2.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - expect(wallet2.keyChainStore.getMasterKeyChain().rootKeyType).to.be.deep.equal('publicKey'); + expect(wallet2.keyChain.type).to.be.deep.equal('publicKey'); expect(wallet2.passphrase).to.be.deep.equal(null); expect(wallet2.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet2.injectDefaultPlugins).to.be.deep.equal(true); @@ -170,7 +170,7 @@ describe('Wallet - class', function suite() { }); }); describe('Wallet - Get/Create Account', function suite() { - this.timeout(10000); + this.timeout(15000); const wallet1 = new Wallet({ mnemonic: fluidMnemonic.mnemonic, ...mocks }); it('should be able to create/get a wallet', async () => { @@ -246,5 +246,6 @@ describe('Wallet - Get/Create Account', function suite() { it('should not leak', () => { const mockOpts1 = { }; fromHDPublicKey.call(mockOpts1, gatherSail.testnet.external.hdpubkey); + expect(mockOpts1.keyChain.keys).to.deep.equal({}); }); }); diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js index 2397a91aab9..c69c7749b9d 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.js @@ -1,18 +1,14 @@ const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); -const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); /** * @param address */ -module.exports = function fromAddress(address, network) { +module.exports = function fromAddress(address) { 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, network }); - this.keyChainStore = new KeyChainStore(); - this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); + this.keyChain = new KeyChain({ address }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js index f0839308fc9..44bd996a4d2 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromAddress.spec.js @@ -18,20 +18,18 @@ describe('Wallet - fromAddress', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.ADDRESS); expect(self1.mnemonic).to.equal(null); expect(self1.address).to.equal(cR4t6ePublicKey.toAddress().toString()); - - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('address'); - expect(keyChain.rootKey).to.equal(cR4t6ePublicKey.toAddress().toString()); + expect(self1.keyChain.type).to.equal('address'); + expect(self1.keyChain.address).to.equal(cR4t6ePublicKey.toAddress().toString()); + expect(self1.keyChain.keys).to.deep.equal({}); const self2 = {}; fromAddress.call(self2, cR4t6ePublicKey.toAddress().toString()); expect(self2.walletType).to.equal(WALLET_TYPES.ADDRESS); expect(self2.mnemonic).to.equal(null); expect(self2.address).to.equal(cR4t6ePublicKey.toAddress().toString()); - - const keyChain2 = self2.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('address'); - expect(keyChain.rootKey).to.equal(cR4t6ePublicKey.toAddress().toString()); + expect(self2.keyChain.type).to.equal('address'); + expect(self2.keyChain.address).to.equal(cR4t6ePublicKey.toAddress().toString()); + expect(self2.keyChain.keys).to.deep.equal({}); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js index 90b6f1a9061..da2aba6e047 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.js @@ -3,7 +3,6 @@ const { is, } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); -const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); const { WALLET_TYPES } = require('../../../CONSTANTS'); /** @@ -15,8 +14,5 @@ module.exports = function fromHDPrivateKey(hdPrivateKey) { this.walletType = WALLET_TYPES.HDWALLET; this.mnemonic = null; this.HDPrivateKey = HDPrivateKey(hdPrivateKey); - - const keyChain = new KeyChain({ HDPrivateKey: this.HDPrivateKey }); - this.keyChainStore = new KeyChainStore(); - this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); + this.keyChain = new KeyChain({ HDPrivateKey: hdPrivateKey }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js index c947a076b4c..5c277483190 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPrivateKey.spec.js @@ -16,10 +16,9 @@ describe('Wallet - fromHDPrivateKey', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(null); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); + expect(self1.keyChain.type).to.equal('HDPrivateKey'); + expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); + expect(self1.keyChain.keys).to.deep.equal({}); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js index aa6a84078a1..0631c051151 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.js @@ -2,7 +2,6 @@ const Dashcore = require('@dashevo/dashcore-lib'); const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); -const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); const normalizeHDPubKey = (key) => (is.string(key) ? Dashcore.HDPublicKey(key) : key); /** @@ -14,8 +13,5 @@ module.exports = function fromHDPublicKey(_hdPublicKey) { this.walletType = WALLET_TYPES.HDPUBLIC; this.mnemonic = null; this.HDPublicKey = normalizeHDPubKey(_hdPublicKey); - - const keyChain = new KeyChain({ HDPublicKey: this.HDPublicKey }); - this.keyChainStore = new KeyChainStore(); - this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); + this.keyChain = new KeyChain({ HDPublicKey: this.HDPublicKey }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js index fca3387d34e..96a1c9cf847 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromHDPublicKey.spec.js @@ -30,10 +30,9 @@ describe('Wallet - HDPublicKey', function suite() { expect(mockOpts1.mnemonic).to.equal(null); expect(mockOpts1.HDPublicKey.toString()).to.equal(gatherTestnet.external.hdpubkey); expect(new Dashcore.HDPublicKey(mockOpts1.HDPublicKey)).to.equal(mockOpts1.HDPublicKey); - - const keyChain = mockOpts1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('HDPublicKey'); - expect(keyChain.rootKey).to.deep.equal(Dashcore.HDPublicKey(gatherTestnet.external.hdpubkey)); + expect(mockOpts1.keyChain.type).to.equal('HDPublicKey'); + expect(mockOpts1.keyChain.HDPublicKey).to.deep.equal(Dashcore.HDPublicKey(gatherTestnet.external.hdpubkey)); + expect(mockOpts1.keyChain.keys).to.deep.equal({}); }); it('should work from a HDPubKey', () => { const wallet1 = new Wallet( @@ -46,9 +45,7 @@ describe('Wallet - HDPublicKey', function suite() { expect(wallet1.plugins).to.be.deep.equal({}); expect(wallet1.accounts).to.be.deep.equal([]); expect(wallet1.network).to.be.deep.equal(Dashcore.Networks.testnet.toString()); - - const keyChain = wallet1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.be.deep.equal('HDPublicKey'); + expect(wallet1.keyChain.type).to.be.deep.equal('HDPublicKey'); expect(wallet1.passphrase).to.be.deep.equal(null); expect(wallet1.allowSensitiveOperations).to.be.deep.equal(false); expect(wallet1.injectDefaultPlugins).to.be.deep.equal(true); @@ -57,28 +54,22 @@ describe('Wallet - HDPublicKey', function suite() { expect(wallet1.exportWallet()).to.be.equal(gatherTestnet.external.hdpubkey); - // FIXME: it appears we had introduced a bug here, - // as it is not possible to have a HDPublicKey derivation with hardened - // Either our path is m/44/1/0/0/0 or it is m/0/0. - // We should clarify this before merging TODO - wallet1 - .getAccount() - .then((account)=>{ - const unusedAddress = account.getUnusedAddress(); - const expectedUnused = { - path: "m/0/0", - index: 0, - address: 'yNJ3xxTXXBBf39VfMBbBuLH2k57uAwxBxj', - transactions: [], - balanceSat: 0, - unconfirmedBalanceSat: 0, - utxos: {}, - fetchedLast: 0, - used: false, - }; - expect(unusedAddress).to.deep.equal(expectedUnused); + wallet1.getAccount().then((account)=>{ + const unusedAddress = account.getUnusedAddress(); + const expectedUnused = { + path: "m/44'/1'/0'/0/0", + index: 0, + address: 'yNJ3xxTXXBBf39VfMBbBuLH2k57uAwxBxj', + transactions: [], + balanceSat: 0, + unconfirmedBalanceSat: 0, + utxos: {}, + fetchedLast: 0, + used: false, + }; + expect(unusedAddress).to.deep.equal(expectedUnused); - wallet1.disconnect(); - }) + wallet1.disconnect(); + }) }); }); diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js index 1e2cbdda7a3..4bdeaefefa8 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.js @@ -3,25 +3,19 @@ const { is, } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); -const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); const { WALLET_TYPES } = require('../../../CONSTANTS'); /** * Will set a wallet to work with a mnemonic (keychain, walletType & HDPrivateKey) * @param mnemonic */ -module.exports = function fromMnemonic(mnemonic, network, passphrase = '') { +module.exports = function fromMnemonic(mnemonic) { 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; - // 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 }); - this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); + this.mnemonic = trimmedMnemonic; // todo : What about without this ? + this.HDPrivateKey = mnemonicToHDPrivateKey(trimmedMnemonic, this.network, this.passphrase); + this.keyChain = new KeyChain({ HDPrivateKey: this.HDPrivateKey }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js index 85f498c6172..14e81028618 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromMnemonic.spec.js @@ -15,29 +15,27 @@ describe('Wallet - fromMnemonic', function suite() { const self1 = { network: 'livenet', }; - fromMnemonic.call(self1, knifeFixture.mnemonic, 'livenet'); + fromMnemonic.call(self1, knifeFixture.mnemonic); expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(knifeFixture.mnemonic); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); expect(new Dashcore.HDPrivateKey(self1.HDPrivateKey)).to.equal(self1.HDPrivateKey); - - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain.network).to.equal('livenet'); - expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); + expect(self1.keyChain.type).to.equal('HDPrivateKey'); + expect(self1.keyChain.network.name).to.equal('livenet'); + expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); + expect(self1.keyChain.keys).to.deep.equal({}); const self2 = {}; fromMnemonic.call(self2, knifeFixture.mnemonic); expect(self2.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self2.mnemonic).to.equal(knifeFixture.mnemonic); - - const keyChain2 = self2.keyChainStore.getMasterKeyChain() - expect(keyChain2.network).to.equal('testnet'); + expect(self2.keyChain.network.name).to.equal('testnet'); expect(self2.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); expect(new Dashcore.HDPrivateKey(self2.HDPrivateKey)).to.equal(self2.HDPrivateKey); - expect(keyChain2.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain2.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); + expect(self2.keyChain.type).to.equal('HDPrivateKey'); + expect(self2.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); + expect(self2.keyChain.keys).to.deep.equal({}); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ @@ -57,35 +55,38 @@ describe('Wallet - fromMnemonic - with passphrase', function suite() { this.timeout(10000); it('should correctly works with passphrase', () => { const self1 = { + network: 'livenet', + passphrase: knifeFixture.passphrase, }; - fromMnemonic.call(self1, knifeFixture.mnemonic, 'livenet', knifeFixture.passphrase); + fromMnemonic.call(self1, knifeFixture.mnemonic); expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(knifeFixture.mnemonic); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyMainnet); expect(new Dashcore.HDPrivateKey(self1.HDPrivateKey)).to.equal(self1.HDPrivateKey); - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain.network).to.equal('livenet'); - expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyMainnet); + expect(self1.keyChain.type).to.equal('HDPrivateKey'); + expect(self1.keyChain.network.name).to.equal('livenet'); + expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyMainnet); + expect(self1.keyChain.keys).to.deep.equal({}); const path1 = 'm/44\'/5\'/0\'/0/0'; - const pubKey1 = keyChain.getForPath(path1).key.publicKey.toAddress(); + const pubKey1 = self1.keyChain.getKeyForPath(path1).publicKey.toAddress(); expect(new Dashcore.Address(pubKey1).toString()).to.equal('Xq3zjky18WjwAHpLgGLasvX5g8TeLRKaxt'); const self2 = { + passphrase: knifeFixture.passphrase, }; - fromMnemonic.call(self2, knifeFixture.mnemonic, 'testnet', knifeFixture.passphrase); + fromMnemonic.call(self2, knifeFixture.mnemonic); expect(self2.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self2.mnemonic).to.equal(knifeFixture.mnemonic); expect(self2.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyTestnet); expect(new Dashcore.HDPrivateKey(self2.HDPrivateKey)).to.equal(self2.HDPrivateKey); - const keyChain2 = self2.keyChainStore.getMasterKeyChain() - expect(keyChain2.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain2.network).to.equal('testnet'); - expect(keyChain2.rootKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyTestnet); + expect(self2.keyChain.type).to.equal('HDPrivateKey'); + expect(self2.keyChain.network.name).to.equal('testnet'); + expect(self2.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootEncryptedPrivateKeyTestnet); + expect(self2.keyChain.keys).to.deep.equal({}); const path2 = 'm/44\'/1\'/0\'/0/0'; - const pubKey2 = keyChain2.getForPath(path2).key.publicKey.toAddress(); + const pubKey2 = self2.keyChain.getKeyForPath(path2).publicKey.toAddress(); expect(new Dashcore.Address(pubKey2, 'testnet').toString()).to.equal('yWYCH9XDRnpdNxh67jQJFkovToBVwWr8Ck'); }); }); diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js index 870af9937d8..2f8f919bc7e 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.js @@ -1,19 +1,15 @@ const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); -const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); /** * Will set a wallet to work with a mnemonic (keychain, walletType & HDPrivateKey) * @param privateKey */ -module.exports = function fromPrivateKey(privateKey, network) { +module.exports = function fromPrivateKey(privateKey) { 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, network }); - this.keyChainStore = new KeyChainStore(); - this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); + this.keyChain = new KeyChain({ privateKey }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js index 4b6ca5e89ab..c707c2faddc 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPrivateKey.spec.js @@ -16,18 +16,18 @@ describe('Wallet - fromPrivateKey', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.PRIVATEKEY); expect(self1.mnemonic).to.equal(null); expect(self1.privateKey).to.equal(cR4t6eFixture.privateKey); - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('privateKey'); - expect(keyChain.rootKey.toWIF()).to.equal(cR4t6eFixture.privateKey); + expect(self1.keyChain.type).to.equal('privateKey'); + expect(self1.keyChain.privateKey).to.equal(cR4t6eFixture.privateKey); + expect(self1.keyChain.keys).to.deep.equal({}); const self2 = {}; fromPrivateKey.call(self2, cR4t6eFixture.privateKey); expect(self2.walletType).to.equal(WALLET_TYPES.PRIVATEKEY); expect(self2.mnemonic).to.equal(null); expect(self2.privateKey).to.equal(cR4t6eFixture.privateKey); - const keyChain2 = self2.keyChainStore.getMasterKeyChain() - expect(keyChain2.rootKeyType).to.equal('privateKey'); - expect(keyChain2.rootKey.toWIF()).to.equal(cR4t6eFixture.privateKey); + expect(self2.keyChain.type).to.equal('privateKey'); + expect(self2.keyChain.privateKey).to.equal(cR4t6eFixture.privateKey); + expect(self2.keyChain.keys).to.deep.equal({}); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js index ffbf41344c9..9e6867ac78f 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.js @@ -1,19 +1,15 @@ const { is } = require('../../../utils'); const KeyChain = require('../../KeyChain/KeyChain'); const { WALLET_TYPES } = require('../../../CONSTANTS'); -const KeyChainStore = require('../../KeyChainStore/KeyChainStore'); /** * Will set a wallet to work with a mnemonic (keychain, walletType & HDPrivateKey) * @param privateKey */ -module.exports = function fromPublicKey(publicKey, network) { +module.exports = function fromPublicKey(publicKey) { 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, network }); - this.keyChainStore = new KeyChainStore(); - this.keyChainStore.addKeyChain(keyChain, { isMasterKeyChain: true }); + this.keyChain = new KeyChain({ publicKey }); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js index 07890f2d45d..8852ee6c6ee 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromPublicKey.spec.js @@ -18,18 +18,18 @@ describe('Wallet - fromPublicKey', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.PUBLICKEY); expect(self1.mnemonic).to.equal(null); expect(self1.publicKey).to.equal(cR4t6ePublicKey); - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('publicKey'); - expect(keyChain.rootKey.toString()).to.equal(cR4t6ePublicKey.toString()); + expect(self1.keyChain.type).to.equal('publicKey'); + expect(self1.keyChain.publicKey).to.equal(cR4t6ePublicKey); + expect(self1.keyChain.keys).to.deep.equal({}); const self2 = {}; fromPublicKey.call(self2, cR4t6ePublicKey.toString()); expect(self2.walletType).to.equal(WALLET_TYPES.PUBLICKEY); expect(self2.mnemonic).to.equal(null); expect(self2.publicKey).to.equal(cR4t6ePublicKey.toString()); - const keyChain2 = self2.keyChainStore.getMasterKeyChain() - expect(keyChain2.rootKeyType).to.equal('publicKey'); - expect(keyChain2.rootKey.toString()).to.equal(cR4t6ePublicKey.toString()); + expect(self2.keyChain.type).to.equal('publicKey'); + expect(self2.keyChain.publicKey).to.equal(cR4t6ePublicKey.toString()); + expect(self2.keyChain.keys).to.deep.equal({}); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.js b/packages/wallet-lib/src/types/Wallet/methods/fromSeed.js index 038fc138e6b..978adc9ecda 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.js +++ b/packages/wallet-lib/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, network) { +module.exports = function fromSeed(seed) { if (!is.seed(seed)) throw new Error('Expected a valid seed (typeof string)'); - return this.fromHDPrivateKey(seedToHDPrivateKey(seed, network)); + return this.fromHDPrivateKey(seedToHDPrivateKey(seed, this.network)); }; diff --git a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js b/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js index a0a9ec827ca..75e9d32cda1 100644 --- a/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js +++ b/packages/wallet-lib/src/types/Wallet/methods/fromSeed.spec.js @@ -19,22 +19,22 @@ describe('Wallet - fromSeed', function suite() { expect(self1.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self1.mnemonic).to.equal(null); expect(self1.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); - const keyChain = self1.keyChainStore.getMasterKeyChain() - expect(keyChain.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); + expect(self1.keyChain.type).to.equal('HDPrivateKey'); + expect(self1.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyTestnet); + expect(self1.keyChain.keys).to.deep.equal({}); const self2 = { fromHDPrivateKey, network: 'mainnet', }; - fromSeed.call(self2, knifeFixture.seed, self2.network); + fromSeed.call(self2, knifeFixture.seed); expect(self2.walletType).to.equal(WALLET_TYPES.HDWALLET); expect(self2.mnemonic).to.equal(null); expect(self2.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); - const keyChain2 = self2.keyChainStore.getMasterKeyChain() - expect(keyChain2.rootKeyType).to.equal('HDPrivateKey'); - expect(keyChain2.rootKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); + expect(self2.keyChain.type).to.equal('HDPrivateKey'); + expect(self2.keyChain.HDPrivateKey.toString()).to.equal(knifeFixture.HDRootPrivateKeyMainnet); + expect(self2.keyChain.keys).to.deep.equal({}); }); it('should reject invalid mnemonic', () => { const invalidInputs = [ diff --git a/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js b/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js index a4c84cbc784..318d7959f8f 100644 --- a/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js +++ b/packages/wallet-lib/src/utils/bip44/ensureAddressesToGapLimit.js @@ -1,5 +1,5 @@ const logger = require('../../logger'); -const { BIP44_ADDRESS_GAP, WALLET_TYPES } = require('../../CONSTANTS'); +const { BIP44_ADDRESS_GAP } = require('../../CONSTANTS'); const is = require('../is'); const getMissingIndexes = require('./getMissingIndexes'); @@ -77,15 +77,13 @@ function ensureAccountAddressesToGapLimit(walletStore, walletType, accountIndex, const gapBetweenLastUsedAndLastGenerated = { external: lastGeneratedIndexes.external - lastUsedIndexes.external, + internal: lastGeneratedIndexes.internal - lastUsedIndexes.internal, }; const addressesToGenerate = { external: BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.external, + internal: BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.internal, }; - if (walletType.includes(WALLET_TYPES.HDWALLET)) { - // eslint-disable-next-line max-len - gapBetweenLastUsedAndLastGenerated.internal = lastGeneratedIndexes.internal - lastUsedIndexes.internal; - addressesToGenerate.internal = BIP44_ADDRESS_GAP - gapBetweenLastUsedAndLastGenerated.internal; - } + Object.entries(addressesToGenerate) .forEach(([typeToGenerate, numberToGenerate]) => { if (numberToGenerate > 0) { From 17d06710e5299336245f6982becff9534a18cda6 Mon Sep 17 00:00:00 2001 From: "markin.io" Date: Mon, 7 Feb 2022 17:43:53 +0700 Subject: [PATCH 3/3] Updates DerivableKeyChain.spec.js --- .../DerivableKeyChain.spec.js | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js index 98dfc69bb5b..02ae0c03abd 100644 --- a/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js +++ b/packages/wallet-lib/src/types/DerivableKeyChain/DerivableKeyChain.spec.js @@ -7,11 +7,11 @@ let derivableKeyChain; let derivableKeyChain2; const mnemonic = 'during develop before curtain hazard rare job language become verb message travel'; const mnemonic2 = 'birth kingdom trash renew flavor utility donkey gasp regular alert pave layer'; -const pk = '4226d5e2fe8cbfe6f5beb7adf5a5b08b310f6c4a67fc27826779073be6f5699e'; const hdPublicKey = 'xpub661MyMwAqRbcFGB6XSWBsD725rJDUbFUpy4zWe2u22nJ2BxpoHFxtVDfKnTnvVQHohnY7AsVpRTHDv6PyPQTYu1KxFPKw29MAVXPEpz1G7V'; const expectedRootDIP15AccountKey_0 = 'tprv8hRzmheQujhJN5XP2dj955nAFCKeEoSifJRWuutdbwWRtusdDQ426jbp75EqErUSuTxmPyxYmP1TpcF5qdxGhXLNXRLMGsRLG6NFCv1WnaQ'; const expectedRootDIP15AccountKey_1 = 'tprv8hRzmheQujhJQyCtFTuUFHxB3Ag5VLB994zhH4CfxbA41cq73HT2mpYq5M33V54oJyn6g514saxxVJB886G55eYX56J6D6x87UNNT6iQHkR'; const expectedKeyForChild_0 = 'tprv8d4podc2Tg459CH2bwLHXj3vdJFBT2rdsk5Nr1djH7hzHdt5LRdvN6QyFwMiDy7ffRdik7fEVRKKgsHB4F18sh8xF6jFXpKq4sUgGBoSbKw'; + describe('DerivableKeyChain', function suite() { this.timeout(1000); it('should create a DerivableKeyChain', () => { @@ -25,17 +25,14 @@ describe('DerivableKeyChain', function suite() { derivableKeyChain2 = new DerivableKeyChain({ mnemonic: mnemonic2, network: 'livenet' }); }); + it('should generate key for full path', () => { const path = 'm/44\'/1\'/0\'/0/0'; const pk2 = derivableKeyChain.getForPath(path).key; const address = new Dashcore.Address(pk2.publicKey.toAddress()).toString(); expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT'); }); - it('should get hardened feature path', () => { - const hardenedPk = derivableKeyChain.getHardenedBIP44HDKey(); - const pk2 = derivableKeyChain.getForPath('m/44\'/1\'').key; - expect(pk2.toString()).to.equal(hardenedPk.toString()); - }); + it('should get DIP15 account key', function () { const rootDIP15AccountKey_0 = derivableKeyChain.getHardenedDIP15AccountKey(0); expect(rootDIP15AccountKey_0.toString()).to.deep.equal(expectedRootDIP15AccountKey_0); @@ -48,7 +45,7 @@ describe('DerivableKeyChain', function suite() { const contactUniqueId = '0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'; // m/9'/5'/15'/0'/0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a'/0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'/0 - const DIP15ExtPubKey_0 = derivableKeyChain2.getDIP15ExtendedKey(userUniqueId, contactUniqueId, 0, 0, type='HDPublicKey'); + const DIP15ExtPubKey_0 = derivableKeyChain2.getDIP15ExtendedKey(userUniqueId, contactUniqueId, 0, 0, 'HDPublicKey'); expect(DIP15ExtPubKey_0.toString()).to.equal('xpub6LTkTQFSb8KMgMSz4B6sMZLpkQAY6wSTDprDkHDmLwWLpnjxazuxZn13FrSLKUafitsxuaaffM5a49P6aswhpppWUuYW6eFnwBXshR2W2eY'); expect(DIP15ExtPubKey_0.publicKey.toString()).to.equal('038030c88ab0106e1f4af3b939db2bafc56f892554106f08da1ce1f9ef10f807bd') @@ -57,28 +54,33 @@ describe('DerivableKeyChain', function suite() { expect(DIP15ExtPrivKey_0.privateKey.toString()).to.equal('fac40790776d171ee1db90899b5eb2df2f7d2aaf35ad56f07ffb8ed2c57f8e60') expect(DIP15ExtPrivKey_0.publicKey.toString()).to.equal('038030c88ab0106e1f4af3b939db2bafc56f892554106f08da1ce1f9ef10f807bd') + // This comes from the test factor of DIP-15 const userAhash = "0xa11ce14f698b32e9bb306dba7bbbee831263dcf658abeebb39930460ead117e5"; const userBhash = "0xb0b052ff075c5ca3c16c3e20e9ac8223834475cc1324ab07889cb24ce6a62793"; const DIP15ExtKey_1 = derivableKeyChain.getDIP15ExtendedKey(userAhash, userBhash, 0, 0); expect(DIP15ExtKey_1.privateKey.toString()).to.equal('60581b6dca8244d3fb3cfe619b5a22277e5423b01e5285f356981f247e0f4a60') expect(DIP15ExtKey_1.publicKey.toString()).to.equal('03deaac00f721151307fbc7bf80d7b8afab98c1f026d67e5f56b21e2013f551ce6') }); - it('should derive from hardened feature path', () => { + + it('should derive from hardened path and get address', () => { const hardenedHDKey = derivableKeyChain.getHardenedBIP44HDKey(); const pk2 = derivableKeyChain.getForPath(`m/44'/1'`).key; expect(pk2.toString()).to.equal(hardenedHDKey.toString()); expect(hardenedHDKey.toString()).to.deep.equal('tprv8dtrJNytYHRiZY585hmHGbguS6VjGpK49puSB7oXZjLHcQfrAzQkF4ZCxM2DkEbyY85J4EYcZ8EjT5ZCU8ozB727TDdodbfXet5GkGau2RQ'); const derivedPk = hardenedHDKey.deriveChild(0, true).deriveChild(0).deriveChild(0); + // m/44'/1'/0'/0/0 (this is first external address of the account 0) const address = new Dashcore.Address(derivedPk.publicKey.toAddress()).toString(); expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT'); }); + it('should get hardened DIP9FeatureHDKey', function () { const hardenedHDKey = derivableKeyChain.getHardenedDIP9FeatureHDKey(); const pk2 = derivableKeyChain.getForPath(`m/9'/1'`).key; expect(pk2.toString()).to.equal(hardenedHDKey.toString()); expect(hardenedHDKey.toString()).to.deep.equal('tprv8fBJjWoGgCpGRCbyzE9RUA59rmoN1RUijhLnXGL4VHnLxvSe523yVg4GrGzbR6TyXtdynAEh5z8UX55EXt2Cb3xjvrsx2PgTY9BHxzFVkWn'); }); - it('should get key for path', () => { + + it('should get key for path using the HDPrivateKey', () => { const derivableKeyChain2 = new DerivableKeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') }); const keyForChild = derivableKeyChain2.getForPath('m/0').key; expect(keyForChild.toString()).to.equal(expectedKeyForChild_0); @@ -88,18 +90,19 @@ describe('DerivableKeyChain', function suite() { const key0 = derivableKeyChain.getForPath('m/0'); derivableKeyChain.getForPath('m/0').isWatched = true key0.isWatched = true; - const key1 = derivableKeyChain.getForPath('m/1', { isWatched: true }); - const key2 = derivableKeyChain.getForPath('m/2', { isWatched: true }); + derivableKeyChain.getForPath('m/1', { isWatched: false }); + derivableKeyChain.getForPath('m/2', { isWatched: true }); const watchedAddresses = derivableKeyChain.getWatchedAddresses(); let expectedWatchedAddresses = [ derivableKeyChain.getForPath('m/0').address.toString(), - derivableKeyChain.getForPath('m/1').address.toString(), derivableKeyChain.getForPath('m/2').address.toString() ]; expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); }); + it('should get watched addresses', function () { + derivableKeyChain.getForPath('m/1').isWatched = true const watchedAddresses = derivableKeyChain.getWatchedAddresses(); const expectedWatchedAddresses = [ 'ybQDfNwiDjk8ZH5UUmHQzAMEmjbrbK5dAj', @@ -108,33 +111,28 @@ describe('DerivableKeyChain', function suite() { ] expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses); }); - // it('should get watched public keys', function () { - // const watchedPubKeys = derivableKeyChain.getWatchedPublicKeys(); - // const expectedWatchedPubKeys = [ - // '03e6ab8177a7ca2699da4f83ca3c27768fb88b70ae9d6bde1cba8de88355ccf199', - // '0246a870b65153b98e453ff08f7198d06bce2a790286f44d90929aceafafa0673f', - // '025ad16f78f67801e52abe5b512d66e3896d4c2fa3ca3150349437a5dd13519967' - // ] - // expect(watchedPubKeys).to.deep.equal(expectedWatchedPubKeys) - // }); + it('should remove an address from watched addresses', function () { - const data0 = derivableKeyChain.getForPath('m/0', { isWatched: false }); - const data1 = derivableKeyChain.getForPath('m/1'); - const data2 = derivableKeyChain.getForPath('m/2'); - data2.isWatched = false; + derivableKeyChain.getForPath('m/0', { isWatched: false }); + derivableKeyChain.getForPath('m/1'); + const data2 = derivableKeyChain.getForPath('m/2'); + data2.isWatched = false; expect(derivableKeyChain.getWatchedAddresses().length).to.equal(1); }); + it('should get address for path', function (){ const address0_1 = derivableKeyChain.getForPath('m/1').address; expect(address0_1.toString()).to.equal('yhFX5rseJPitV45HUCaa9haeGHtLuooBaq') }) + it('should mark address as used', function () { const address0_0 = derivableKeyChain.getForPath('m/0').address; derivableKeyChain.markAddressAsUsed(address0_0); expect(derivableKeyChain.issuedPaths.get('m/0').isUsed).to.equal(true) }); }); + describe('DerivableKeyChain - HDPublicKey', function suite(){ let hdpubDerivableKeyChain; it('should initiate from a HDPublicKey', function () { @@ -147,18 +145,22 @@ describe('DerivableKeyChain - HDPublicKey', function suite(){ expect(hdpubDerivableKeyChain.keyChainId).to.equal('kc5059442d66'); expect(hdpubDerivableKeyChain.getRootKey().toString()).to.equal(hdPublicKey); }); + it('should derivate', function () { const key0_1 = hdpubDerivableKeyChain.getForPath('m/1').key; expect(key0_1.publicKey.toAddress(hdpubDerivableKeyChain.network).toString()).to.equal('XoL5LcBiDWcj6L7fFwytsFoX5Vz7BVXw9w') }); + it('should get address for path', function (){ const address0_1 = hdpubDerivableKeyChain.getForPath('m/2').address; expect(address0_1.toString()).to.equal('XwAzpxQKbgebaLiadq1c6rDeFJ4FKPUufy') }) }) + describe('DerivableKeyChain - single privateKey', function suite() { this.timeout(10000); - it('should correctly errors out when not a HDPublicKey (privateKey)', () => { + + it('should correctly throw errors out when not a HDPublicKey (privateKey)', () => { const privateKey = Dashcore.PrivateKey().toString(); const network = 'livenet'; const pkDerivableKeyChain = new DerivableKeyChain({ privateKey, network }); @@ -169,6 +171,7 @@ describe('DerivableKeyChain - single privateKey', function suite() { const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPrivateKey, impossible to derivate keys for path m/0'; expect(() => pkDerivableKeyChain.getForPath('m/0')).to.throw(expectedException1); }); + it('should get private key', () => { const privateKey = Dashcore.PrivateKey().toString(); const pkDerivableKeyChain = new DerivableKeyChain({ privateKey, network: 'livenet' });