diff --git a/scripts/global.js b/scripts/global.js index 9261c7742..8cdbafcf5 100644 --- a/scripts/global.js +++ b/scripts/global.js @@ -972,14 +972,16 @@ export async function sweepAddress(arrUTXOs, sweepingMasterKey, nFixedFee = 0) { const nFee = nFixedFee || getNetwork().getFee(cTx.serialize().length); // Use a new address from our wallet to sweep the UTXOs in to - const strAddress = (await getNewAddress(true, false))[0]; + const strAddress = ( + await getNewAddress({ updateGUI: true, verify: false, nReceiving: 1 }) + )[0]; // Sweep the full funds amount, minus the fee, leaving no change from any sweeped UTXOs cTx.addoutput(strAddress, (nTotal - nFee) / COIN); // Sign using the given Master Key, then broadcast the sweep, returning the TXID (or a failure) const sweepingWallet = new Wallet(0, false); - await sweepingWallet.setMasterKey(sweepingMasterKey); + sweepingWallet.setMasterKey(sweepingMasterKey); const sign = await signTransaction(cTx, sweepingWallet); return await getNetwork().sendTransaction(sign); @@ -1963,7 +1965,7 @@ export async function createProposal() { // If Advanced Mode is enabled and an address is given, use the provided address, otherwise, generate a new one const strAddress = document.getElementById('proposalAddress').value.trim() || - (await wallet.getNewAddress())[0]; + wallet.getNewAddress(1)[0]; const nextSuperblock = await Masternode.getNextSuperblock(); const proposal = { name: strTitle, diff --git a/scripts/transactions.js b/scripts/transactions.js index 4656c2d49..19d7b1062 100644 --- a/scripts/transactions.js +++ b/scripts/transactions.js @@ -232,7 +232,7 @@ export async function undelegateGUI() { if (!validateAmount(nAmount)) return; // Generate a new address to undelegate towards - const [address] = wallet.getNewAddress(); + const [address] = wallet.getNewAddress(1); // Perform the TX const cTxRes = await createAndSendTransaction({ @@ -304,6 +304,7 @@ export async function createAndSendTransaction({ const nChange = cCoinControl.nValue - (nFee + amount); const [changeAddress, _] = await getNewAddress({ verify: wallet.isHardwareWallet(), + nReceiving: 1, }); /** @@ -342,8 +343,7 @@ export async function createAndSendTransaction({ // For custom Cold Owner Addresses, it could be an external address, so we need the mempool to class it as an 'external send' const strOwnerAddress = fCustomColdOwner ? delegationOwnerAddress - : (await wallet.getNewAddress())[0]; - const strOwnerPath = await wallet.isOwnAddress(strOwnerAddress); + : wallet.getNewAddress(1)[0]; // Create the Delegation output cTx.addcoldstakingoutput(strOwnerAddress, address, amount / COIN); @@ -425,6 +425,7 @@ export async function createMasternode() { // Generate the Masternode collateral const [address] = await getNewAddress({ verify: wallet.isHardwareWallet(), + nReceiving: 1, }); const result = await createAndSendTransaction({ amount: cChainParams.current.collateralInSats, diff --git a/scripts/wallet.js b/scripts/wallet.js index 9efde1ee3..bfedb90e8 100644 --- a/scripts/wallet.js +++ b/scripts/wallet.js @@ -32,6 +32,12 @@ import { * in future PRs this class will manage balance, UTXOs, masternode etc... */ export class Wallet { + /** + * We are using two chains: The external chain, and the internal one (i.e. change addresses) + * See https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki for more info + * (Change paragraph) + */ + static chains = 2; /** * @type {import('./masterkey.js').MasterKey} */ @@ -40,20 +46,23 @@ export class Wallet { * @type {number} */ #nAccount; + /** + * Map bip48 change -> Loaded index * Number of loaded indexes, loaded means that they are in the ownAddresses map - * @type {number} + * @type {Map} */ - #loadedIndexes = 0; + #loadedIndexes = new Map(); /** + * Map bip48 change -> Highest used index * Highest index used, where used means that the corresponding address is on chain (for example in a tx) - * @type {number} + * @type {Map} */ - #highestUsedIndex = 0; + #highestUsedIndices = new Map(); /** - * @type {number} + * @type {Map} */ - #addressIndex = 0; + #addressIndices = new Map(); /** * Map our own address -> Path * @type {Map} @@ -78,6 +87,10 @@ export class Wallet { this.#nAccount = nAccount; this.#isMainWallet = isMainWallet; this.#lockedCoins = new Set(); + for (let i = 0; i < Wallet.chains; i++) { + this.#highestUsedIndices.set(i, 0); + this.#loadedIndexes.set(i, 0); + } } /** @@ -185,16 +198,22 @@ export class Wallet { if (this.#isMainWallet) { getNetwork().setWallet(this); } - this.loadAddresses(); + for (let i = 0; i < Wallet.chains; i++) this.loadAddresses(i); } /** * Reset the wallet, indexes address map and so on */ reset() { - this.#highestUsedIndex = 0; - this.#loadedIndexes = 0; + this.#highestUsedIndices = new Map(); + this.#loadedIndexes = new Map(); this.#ownAddresses = new Map(); + this.#addressIndices = new Map(); + for (let i = 0; i < Wallet.chains; i++) { + this.#highestUsedIndices.set(i, 0); + this.#loadedIndexes.set(i, 0); + this.#addressIndices.set(i, 0); + } // TODO: This needs to be refactored // The wallet could own its own mempool and network? // Instead of having this isMainWallet flag @@ -210,7 +229,7 @@ export class Wallet { * */ getCurrentAddress() { - return this.getAddress(0, this.#addressIndex); + return this.getAddress(0, this.#addressIndices.get(0)); } /** @@ -302,16 +321,26 @@ export class Wallet { /** * @return [string, string] Address and its BIP32 derivation path */ - getNewAddress() { - const last = this.#highestUsedIndex; - this.#addressIndex = - (this.#addressIndex > last ? this.#addressIndex : last) + 1; - if (this.#addressIndex - last > MAX_ACCOUNT_GAP) { + getNewAddress(nReceiving = 0) { + const last = this.#highestUsedIndices.get(nReceiving); + this.#addressIndices.set( + nReceiving, + (this.#addressIndices.get(nReceiving) > last + ? this.#addressIndices.get(nReceiving) + : last) + 1 + ); + if (this.#addressIndices.get(nReceiving) - last > MAX_ACCOUNT_GAP) { // If the user creates more than ${MAX_ACCOUNT_GAP} empty wallets we will not be able to sync them! - this.#addressIndex = last; + this.#addressIndices.set(nReceiving, last); } - const path = this.getDerivationPath(0, this.#addressIndex); - const address = this.getAddress(0, this.#addressIndex); + const path = this.getDerivationPath( + nReceiving, + this.#addressIndices.get(nReceiving) + ); + const address = this.getAddress( + nReceiving, + this.#addressIndices.get(nReceiving) + ); return [address, path]; } @@ -332,34 +361,38 @@ export class Wallet { ); const path = this.isOwnAddress(address); if (path) { - this.#highestUsedIndex = Math.max( - parseInt(path.split('/')[5]), - this.#highestUsedIndex + const nReceiving = parseInt(path.split('/')[4]); + this.#highestUsedIndices.set( + nReceiving, + Math.max( + parseInt(path.split('/')[5]), + this.#highestUsedIndices.get(nReceiving) + ) ); if ( - this.#highestUsedIndex + MAX_ACCOUNT_GAP >= - this.#loadedIndexes + this.#highestUsedIndices.get(nReceiving) + MAX_ACCOUNT_GAP >= + this.#loadedIndexes.get(nReceiving) ) { - this.loadAddresses(); + this.loadAddresses(nReceiving); } } } /** * Load MAX_ACCOUNT_GAP inside #ownAddresses map. + * @param {number} chain - Chain to load */ - loadAddresses() { + loadAddresses(chain) { if (this.isHD()) { - for ( - let i = this.#loadedIndexes; - i <= this.#loadedIndexes + MAX_ACCOUNT_GAP; - i++ - ) { - const path = this.getDerivationPath(0, i); + const start = this.#loadedIndexes.get(chain); + const end = start + MAX_ACCOUNT_GAP; + for (let i = start; i <= end; i++) { + const path = this.getDerivationPath(chain, i); const address = this.#masterKey.getAddress(path); this.#ownAddresses.set(address, path); } - this.#loadedIndexes += MAX_ACCOUNT_GAP; + + this.#loadedIndexes.set(chain, end); } else { this.#ownAddresses.set(this.getKeyToExport(), ':)'); } @@ -694,8 +727,9 @@ export async function hasEncryptedWallet() { export async function getNewAddress({ updateGUI = false, verify = false, + nReceiving = 0, } = {}) { - const [address, path] = wallet.getNewAddress(); + const [address, path] = wallet.getNewAddress(nReceiving); if (verify && wallet.isHardwareWallet()) { // Generate address to present to the user without asking to verify const confAddress = await confirmPopup({