diff --git a/index.template.html b/index.template.html index 172506e8c..83438a757 100644 --- a/index.template.html +++ b/index.template.html @@ -475,7 +475,7 @@

Export - + Refresh address diff --git a/locale/de/translation.js b/locale/de/translation.js index 4ea5ca087..9aab184a1 100644 --- a/locale/de/translation.js +++ b/locale/de/translation.js @@ -280,6 +280,7 @@ export const de_translation = { proposalFinalisationReady: 'Bereit zum Einreichen', //Ready to submit proposalPassing: 'Bestehend', //PASSING proposalFailing: 'Scheiternd', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'Finanziert', //FUNDED proposalNotFunded: 'nicht Finanziert', //NOT FUNDED proposalPaymentsRemaining: 'Ausstehende Transaktionen
', //installment(s) remaining
of diff --git a/locale/en/translation.js b/locale/en/translation.js index f24274c6a..7cf743cb1 100644 --- a/locale/en/translation.js +++ b/locale/en/translation.js @@ -278,6 +278,7 @@ export const en_translation = { proposalFinalisationReady: 'Ready to submit', proposalPassing: 'PASSING', proposalFailing: 'FAILING', + proposalTooYoung: 'TOO YOUNG', proposalFunded: 'FUNDED', proposalNotFunded: 'NOT FUNDED', proposalPaymentsRemaining: 'installment(s) remaining
of', diff --git a/locale/es-mx/translation.js b/locale/es-mx/translation.js index d0c696cfc..8de684b83 100644 --- a/locale/es-mx/translation.js +++ b/locale/es-mx/translation.js @@ -285,6 +285,7 @@ export const es_mx_translation = { proposalFinalisationReady: 'Listo para enviarla', //Ready to submit proposalPassing: 'PASANDO', //PASSING proposalFailing: 'FRACASANDO', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'FINANCIADA', //FUNDED proposalNotFunded: 'NO FINANCIADA', //NOT FUNDED proposalPaymentsRemaining: 'plazo(s) restante(s)
de', //installment(s) remaining
of diff --git a/locale/fr/translation.js b/locale/fr/translation.js index 1b0bb2acd..ade492811 100644 --- a/locale/fr/translation.js +++ b/locale/fr/translation.js @@ -287,6 +287,7 @@ export const fr_translation = { proposalFinalisationReady: 'Prêt à soumettre', //Ready to submit proposalPassing: 'PASSANT', //PASSING proposalFailing: 'ÉCHOUANT', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'FINANCÉE', //FUNDED proposalNotFunded: 'NON FINANCÉE', //NOT FUNDED proposalPaymentsRemaining: "l'installation(s) restants
de", //installment(s) remaining
of diff --git a/locale/it/translation.js b/locale/it/translation.js index 4b2222026..e87375d8e 100644 --- a/locale/it/translation.js +++ b/locale/it/translation.js @@ -267,6 +267,7 @@ export const it_translation = { proposalFinalisationReady: "Pronto per l'invio", //Ready to submit proposalPassing: 'Passata', //PASSING proposalFailing: 'Fallita', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'Finanziata', //FUNDED proposalNotFunded: 'Non finanziata', //NOT FUNDED proposalPaymentsRemaining: 'rata/i rimanente
di', //installment(s) remaining
of diff --git a/locale/ph/translation.js b/locale/ph/translation.js index 09a85697f..a7a261956 100644 --- a/locale/ph/translation.js +++ b/locale/ph/translation.js @@ -289,6 +289,7 @@ export const ph_translation = { proposalFinalisationReady: 'Handa ng ipasa', //Ready to submit proposalPassing: 'PASSING', //PASSING proposalFailing: 'FAILING', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'FUNDED', //FUNDED proposalNotFunded: 'NOT FUNDED', //NOT FUNDED proposalPaymentsRemaining: 'installment(s) remaining
of', //installment(s) remaining
of diff --git a/locale/pt-br/translation.js b/locale/pt-br/translation.js index 47de6a465..8d8733d14 100644 --- a/locale/pt-br/translation.js +++ b/locale/pt-br/translation.js @@ -283,6 +283,7 @@ export const pt_br_translation = { proposalFinalisationReady: 'Pronto para enviar', //Ready to submit proposalPassing: 'PASSAGEM', //PASSING proposalFailing: 'FALHA', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'FINANCIADO/A', //FUNDED proposalNotFunded: 'NÃO FINANCIADO/A', //NOT FUNDED proposalPaymentsRemaining: 'parcela(s) restante(s)
de', //installment(s) remaining
of diff --git a/locale/pt-pt/translation.js b/locale/pt-pt/translation.js index a6ba4ec07..098ba0b87 100644 --- a/locale/pt-pt/translation.js +++ b/locale/pt-pt/translation.js @@ -283,6 +283,7 @@ export const pt_pt_translation = { proposalFinalisationReady: 'Pronto para enviar', //Ready to submit proposalPassing: 'PASSAGEM', //PASSING proposalFailing: 'FALHA', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: 'FINANCIADO/A', //FUNDED proposalNotFunded: 'NÃO FINANCIADO/A', //NOT FUNDED proposalPaymentsRemaining: 'parcela(s) restante(s)
de', //installment(s) remaining
of diff --git a/locale/template/translation.js b/locale/template/translation.js index c8eb1cb8f..418a38db7 100644 --- a/locale/template/translation.js +++ b/locale/template/translation.js @@ -271,6 +271,7 @@ export const translation_template = { proposalFinalisationReady: '', //Ready to submit proposalPassing: '', //PASSING proposalFailing: '', //FAILING + proposalTooYoung: '', //TOO YOUNG proposalFunded: '', //FUNDED proposalNotFunded: '', //NOT FUNDED proposalPaymentsRemaining: '', //installment(s) remaining
of diff --git a/locale/uwu/translation.js b/locale/uwu/translation.js index e6a4b9a74..be76369c4 100644 --- a/locale/uwu/translation.js +++ b/locale/uwu/translation.js @@ -281,6 +281,7 @@ export const uwu_translation = { proposalFinalisationReady: 'Ready tew submit', //Ready to submit proposalPassing: 'PASSING, YAY!', //PASSING proposalFailing: 'FAILING, NAY!', //FAILING + proposalTooYoung: 'TOO YOUNG, BAKA!', //TOO YOUNG proposalFunded: 'FUNDED!', //FUNDED proposalNotFunded: 'NO MONIES', //NOT FUNDED proposalPaymentsRemaining: 'payment(s) remainingz
of', //installment(s) remaining
of diff --git a/scripts/bitTrx.js b/scripts/bitTrx.js index e70624625..fc7662dd2 100644 --- a/scripts/bitTrx.js +++ b/scripts/bitTrx.js @@ -3,7 +3,8 @@ import * as nobleSecp256k1 from '@noble/secp256k1'; import { BigInteger } from 'biginteger'; import bs58 from 'bs58'; import { OP } from './script.js'; -import { deriveAddress, parseWIF, getDerivationPath } from './wallet.js'; +import { wallet } from './wallet.js'; +import { parseWIF, deriveAddress } from './encoding.js'; import { sha256 } from '@noble/hashes/sha256'; import { getNetwork } from './network.js'; import { cChainParams } from './chain_params.js'; @@ -30,7 +31,7 @@ export default class bitjs { index, script, sequence, - path = getDerivationPath(), + path = wallet.getDerivationPath(), }) { const o = {}; o.outpoint = { hash: txid, index: index }; diff --git a/scripts/chain_params.js b/scripts/chain_params.js index 3a819b2e9..55fc752e6 100644 --- a/scripts/chain_params.js +++ b/scripts/chain_params.js @@ -46,6 +46,7 @@ export const cChainParams = { }, budgetCycleBlocks: 43200, proposalFee: 50 * COIN, + proposalFeeConfirmRequirement: 6, maxPaymentCycles: 6, maxPayment: 10 * 43200 * COIN, // 43200 blocks of 10 PIV }, @@ -74,6 +75,7 @@ export const cChainParams = { }, budgetCycleBlocks: 144, proposalFee: 50 * COIN, + proposalFeeConfirmRequirement: 3, maxPaymentCycles: 20, maxPayment: 10 * 144 * COIN, // 144 blocks of 10 tPIV }, diff --git a/scripts/contacts-book.js b/scripts/contacts-book.js index 5833dda28..47aec4edf 100644 --- a/scripts/contacts-book.js +++ b/scripts/contacts-book.js @@ -13,7 +13,7 @@ import { sanitizeHTML, } from './misc'; import { scanQRCode } from './scanner'; -import { getDerivationPath, hasEncryptedWallet, masterKey } from './wallet'; +import { wallet, hasEncryptedWallet } from './wallet'; /** * Represents an Account contact @@ -343,17 +343,16 @@ export async function guiRenderReceiveModal( let strPubkey = ''; // If HD: use xpub, otherwise we'll fallback to our single address - if (masterKey.isHD) { + if (wallet.isHD()) { // Get our current wallet XPub - const derivationPath = getDerivationPath( - masterKey.isHardwareWallet - ) + const derivationPath = wallet + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); - strPubkey = await masterKey.getxpub(derivationPath); + strPubkey = await wallet.getMasterKey().getxpub(derivationPath); } else { - strPubkey = await masterKey.getCurrentAddress(); + strPubkey = await wallet.getMasterKey().getCurrentAddress(); } // Construct the Contact Share URI @@ -375,7 +374,7 @@ export async function guiRenderReceiveModal( document.getElementById('clipboard').value = strPubkey; } else { // Get our current wallet address - const strAddress = await masterKey.getCurrentAddress(); + const strAddress = await wallet.getMasterKey().getCurrentAddress(); // Update the QR Label (we'll show the address here for now, user can set Contact "Name" optionally later) doms.domModalQrLabel.innerHTML = @@ -405,7 +404,7 @@ export async function guiRenderReceiveModal( } } else if (cReceiveType === RECEIVE_TYPES.ADDRESS) { // Get our current wallet address - const strAddress = await masterKey.getCurrentAddress(); + const strAddress = await wallet.getMasterKey().getCurrentAddress(); createQR('pivx:' + strAddress, doms.domModalQR); doms.domModalQrLabel.innerHTML = strAddress + @@ -416,11 +415,12 @@ export async function guiRenderReceiveModal( document.getElementById('clipboard').value = strAddress; } else { // Get our current wallet XPub - const derivationPath = getDerivationPath(masterKey.isHardwareWallet) + const derivationPath = wallet + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); - const strXPub = await masterKey.getxpub(derivationPath); + const strXPub = await wallet.getMasterKey().getxpub(derivationPath); // Update the QR Label (we'll show the address here for now, user can set Contact "Name" optionally later) doms.domModalQrLabel.innerHTML = @@ -468,7 +468,7 @@ export let cReceiveType = RECEIVE_TYPES.CONTACT; */ export async function guiToggleReceiveType(nForceType = null) { // Figure out which Types can be used with this wallet - const nTypeMax = masterKey.isHD ? 3 : 2; + const nTypeMax = wallet.isHD() ? 3 : 2; // Loop back to the first if we hit the end cReceiveType = @@ -521,13 +521,16 @@ export async function guiAddContact() { // Ensure we're not adding our own XPub if (isXPub(strAddr)) { - if (masterKey.isHD) { - const derivationPath = getDerivationPath(masterKey.isHardwareWallet) + if (wallet.isHD()) { + const derivationPath = wallet + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); // Compare the XPub against our own - const fOurs = strAddr === (await masterKey.getxpub(derivationPath)); + const fOurs = + strAddr === + (await wallet.getMasterKey().getxpub(derivationPath)); if (fOurs) { createAlert( 'warning', @@ -539,7 +542,7 @@ export async function guiAddContact() { } } else { // Ensure we're not adding (one of) our own address(es) - if (await masterKey.isOwnAddress(strAddr)) { + if (await wallet.isOwnAddress(strAddr)) { createAlert('warning', ALERTS.CONTACTS_CANNOT_ADD_YOURSELF, 3500); return false; } @@ -617,14 +620,16 @@ export async function guiAddContactPrompt( // Ensure we're not adding our own XPub if (isXPub(strPubkey)) { - if (masterKey.isHD) { - const derivationPath = getDerivationPath(masterKey.isHardwareWallet) + if (wallet.isHD()) { + const derivationPath = wallet + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); // Compare the XPub against our own const fOurs = - strPubkey === (await masterKey.getxpub(derivationPath)); + strPubkey === + (await wallet.getMasterKey().getxpub(derivationPath)); if (fOurs) { createAlert( 'warning', @@ -636,7 +641,7 @@ export async function guiAddContactPrompt( } } else { // Ensure we're not adding (one of) our own address(es) - if (await masterKey.isOwnAddress(strPubkey)) { + if (await wallet.isOwnAddress(strPubkey)) { createAlert('warning', ALERTS.CONTACTS_CANNOT_ADD_YOURSELF, 3500); return false; } @@ -978,15 +983,16 @@ export async function localContactToURI(account, pubkey) { // If HD: use xpub, otherwise we'll fallback to our single address if (!strPubkey) { - if (masterKey.isHD) { + if (wallet.isHD()) { // Get our current wallet XPub - const derivationPath = getDerivationPath(masterKey.isHardwareWallet) + const derivationPath = wallet + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); - strPubkey = await masterKey.getxpub(derivationPath); + strPubkey = await wallet.getMasterKey().getxpub(derivationPath); } else { - strPubkey = await masterKey.getCurrentAddress(); + strPubkey = await wallet.getMasterKey().getCurrentAddress(); } } diff --git a/scripts/encoding.js b/scripts/encoding.js new file mode 100644 index 000000000..d9cd589e4 --- /dev/null +++ b/scripts/encoding.js @@ -0,0 +1,187 @@ +import { sha256 } from '@noble/hashes/sha256'; +import { hexToBytes, bytesToHex, dSHA256 } from './utils.js'; +import * as nobleSecp256k1 from '@noble/secp256k1'; +import { ripemd160 } from '@noble/hashes/ripemd160'; +import { cChainParams, PRIVKEY_BYTE_LENGTH } from './chain_params.js'; +import { + pubKeyHashNetworkLen, + writeToUint8, + getSafeRand, + pubPrebaseLen, +} from './misc.js'; + +import bs58 from 'bs58'; + +/** + * Compress an uncompressed Public Key in byte form + * @param {Array | Uint8Array} pubKeyBytes - The uncompressed public key bytes + * @returns {Array} The compressed public key bytes + */ +export function compressPublicKey(pubKeyBytes) { + if (pubKeyBytes.length != 65) + throw new Error('Attempting to compress an invalid uncompressed key'); + const x = pubKeyBytes.slice(1, 33); + const y = pubKeyBytes.slice(33); + + // Compressed key is [key_parity + 2, x] + return [y[31] % 2 === 0 ? 2 : 3, ...x]; +} + +/** + * Network encode 32 bytes for a private key + * @param {Uint8Array} pkBytes - 32 Bytes + * @returns {Uint8Array} - The network-encoded Private Key bytes + */ +export function encodePrivkeyBytes(pkBytes) { + const pkNetBytes = new Uint8Array(pkBytes.length + 2); + pkNetBytes[0] = cChainParams.current.SECRET_KEY; // Private key prefix (1 byte) + writeToUint8(pkNetBytes, pkBytes, 1); // Private key bytes (32 bytes) + pkNetBytes[pkNetBytes.length - 1] = 1; // Leading digit (1 byte) + return pkNetBytes; +} + +/** + * Generate a new private key OR encode an existing private key from raw bytes + * @param {Uint8Array} pkBytesToEncode - Bytes to encode as a coin private key + * @returns {PrivateKey} - The private key + */ +export function generateOrEncodePrivkey(pkBytesToEncode) { + // Private Key Generation + const pkBytes = pkBytesToEncode || getSafeRand(); + + // Network Encoding + const pkNetBytes = encodePrivkeyBytes(pkBytes); + + // Double SHA-256 hash + const shaObj = dSHA256(pkNetBytes); + + // WIF Checksum + const checksum = shaObj.slice(0, 4); + const keyWithChecksum = new Uint8Array(34 + checksum.length); + writeToUint8(keyWithChecksum, pkNetBytes, 0); + writeToUint8(keyWithChecksum, checksum, 34); + + // Return both the raw bytes and the WIF format + return { pkBytes, strWIF: bs58.encode(keyWithChecksum) }; +} + +/** + * Derive a Secp256k1 network-encoded public key (coin address) from raw private or public key bytes + * @param {Object} options - The object to deconstruct + * @param {String} [options.publicKey] - The hex encoded public key. Can be both compressed or uncompressed + * @param {Array | Uint8Array} [options.pkBytes] - An array of bytes containing the private key + * @param {"ENCODED" | "UNCOMPRESSED_HEX" | "COMPRESSED_HEX"} options.output - Output + * @return {String} the public key with the specified encoding + */ +export function deriveAddress({ pkBytes, publicKey, output = 'ENCODED' }) { + if (!pkBytes && !publicKey) return null; + const compress = output !== 'UNCOMPRESSED_HEX'; + // Public Key Derivation + let pubKeyBytes = publicKey + ? hexToBytes(publicKey) + : nobleSecp256k1.getPublicKey(pkBytes, compress); + + if (output === 'UNCOMPRESSED_HEX') { + if (pubKeyBytes.length !== 65) { + // It's actually possible, but it's probably not something that we'll need + throw new Error("Can't uncompress an already compressed key"); + } + return bytesToHex(pubKeyBytes); + } + + if (pubKeyBytes.length === 65) { + pubKeyBytes = compressPublicKey(pubKeyBytes); + } + + if (pubKeyBytes.length != 33) { + throw new Error('Invalid public key'); + } + + if (output === 'COMPRESSED_HEX') { + return bytesToHex(pubKeyBytes); + } + + // First pubkey SHA-256 hash + const pubKeyHashing = sha256(new Uint8Array(pubKeyBytes)); + + // RIPEMD160 hash + const pubKeyHashRipemd160 = ripemd160(pubKeyHashing); + + // Network Encoding + const pubKeyHashNetwork = new Uint8Array(pubKeyHashNetworkLen); + pubKeyHashNetwork[0] = cChainParams.current.PUBKEY_ADDRESS; + writeToUint8(pubKeyHashNetwork, pubKeyHashRipemd160, 1); + + // Double SHA-256 hash + const pubKeyHashingSF = dSHA256(pubKeyHashNetwork); + + // Checksum + const checksumPubKey = pubKeyHashingSF.slice(0, 4); + + // Public key pre-base58 + const pubKeyPreBase = new Uint8Array(pubPrebaseLen); + writeToUint8(pubKeyPreBase, pubKeyHashNetwork, 0); + writeToUint8(pubKeyPreBase, checksumPubKey, pubKeyHashNetworkLen); + + // Encode as Base58 human-readable network address + return bs58.encode(pubKeyPreBase); +} + +// Verify the integrity of a WIF private key, optionally parsing and returning the key payload +export function verifyWIF( + strWIF = '', + fParseBytes = false, + skipVerification = false +) { + // Convert from Base58 + const bWIF = bs58.decode(strWIF); + + if (!skipVerification) { + // Verify the byte length + if (bWIF.byteLength !== PRIVKEY_BYTE_LENGTH) { + throw Error( + 'Private key length (' + + bWIF.byteLength + + ') is invalid, should be ' + + PRIVKEY_BYTE_LENGTH + + '!' + ); + } + + // Verify the network byte + if (bWIF[0] !== cChainParams.current.SECRET_KEY) { + // Find the network it's trying to use, if any + const cNetwork = Object.keys(cChainParams) + .filter((strNet) => strNet !== 'current') + .map((strNet) => cChainParams[strNet]) + .find((cNet) => cNet.SECRET_KEY === bWIF[0]); + // Give a specific alert based on the byte properties + throw Error( + cNetwork + ? 'This private key is for ' + + (cNetwork.isTestnet ? 'Testnet' : 'Mainnet') + + ', wrong network!' + : 'This private key belongs to another coin, or is corrupted.' + ); + } + + // Perform SHA256d hash of the WIF bytes + const shaHash = dSHA256(bWIF.slice(0, 34)); + + // Verify checksum (comparison by String since JS hates comparing object-like primitives) + const bChecksumWIF = bWIF.slice(bWIF.byteLength - 4); + const bChecksum = shaHash.slice(0, 4); + if (bChecksumWIF.join('') !== bChecksum.join('')) { + throw Error( + 'Private key checksum is invalid, key may be modified, mis-typed, or corrupt.' + ); + } + } + + return fParseBytes ? Uint8Array.from(bWIF.slice(1, 33)) : true; +} + +// A convenient alias to verifyWIF that returns the raw byte payload +export function parseWIF(strWIF, skipVerification = false) { + return verifyWIF(strWIF, true, skipVerification); +} diff --git a/scripts/global.js b/scripts/global.js index 505c51b0a..c18a4a8da 100644 --- a/scripts/global.js +++ b/scripts/global.js @@ -3,15 +3,12 @@ import Masternode from './masternode.js'; import { ALERTS, tr, start as i18nStart, translation } from './i18n.js'; import * as jdenticon from 'jdenticon'; import { - masterKey, + wallet, hasEncryptedWallet, importWallet, - encryptWallet, decryptWallet, - getNewAddress, - getDerivationPath, - LegacyMasterKey, } from './wallet.js'; +import { LegacyMasterKey } from './masterkey.js'; import { getNetwork, HistoricalTxType } from './network.js'; import { start as settingsStart, @@ -845,7 +842,7 @@ export async function createActivityListHTML(arrTXs, fRewards = false) { // Generate the TX list for (const cTx of arrTXs) { // If no account is loaded, we render nothing! - if (!masterKey) break; + if (!wallet.isLoaded()) break; const dateTime = new Date(cTx.time * 1000); @@ -905,7 +902,7 @@ export async function createActivityListHTML(arrTXs, fRewards = false) { // Check all addresses to find our own, caching them for performance for (const strAddr of cTx.receivers.concat(cTx.senders)) { // If a previous Tx checked this address, skip it, otherwise, check it against our own address(es) - if (!(await masterKey.isOwnAddress(strAddr))) { + if (!(await wallet.isOwnAddress(strAddr))) { // External address, this is not a self-only Tx fSendToSelf = false; } @@ -929,7 +926,7 @@ export async function createActivityListHTML(arrTXs, fRewards = false) { const arrExternalAddresses = ( await Promise.all( cTx.receivers.map(async (addr) => [ - await masterKey.isOwnAddress(addr), + await wallet.isOwnAddress(addr), addr, ]) ) @@ -964,7 +961,7 @@ export async function createActivityListHTML(arrTXs, fRewards = false) { const arrExternalAddresses = ( await Promise.all( cTx.senders.map(async (addr) => [ - await masterKey.isOwnAddress(addr), + await wallet.isOwnAddress(addr), addr, ]) ) @@ -1118,15 +1115,17 @@ export async function renderActivityGUI(arrTXs) { * @param {string?} strAddress - Optional address to open, if void, the master key is used */ export async function openExplorer(strAddress = '') { - if (masterKey?.isHD && !strAddress) { - const derivationPath = getDerivationPath(masterKey.isHardwareWallet) + if (wallet.isLoaded() && wallet.isHD() && !strAddress) { + const derivationPath = wallet + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); - const xpub = await masterKey.getxpub(derivationPath); + const xpub = await wallet.getMasterKey().getxpub(derivationPath); window.open(cExplorer.url + '/xpub/' + xpub, '_blank'); } else { - const address = strAddress || (await masterKey.getAddress()); + const address = + strAddress || (await wallet.getMasterKey().getAddress()); window.open(cExplorer.url + '/address/' + address, '_blank'); } } @@ -1353,10 +1352,10 @@ export async function govVote(hash, voteCode) { */ export async function startMasternode(fRestart = false) { const database = await Database.getInstance(); - const cMasternode = await database.getMasternode(masterKey); + const cMasternode = await database.getMasternode(wallet.getMasterKey()); if (cMasternode) { if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet(translation.walletUnlockMNStart)) ) return; @@ -1375,8 +1374,8 @@ export async function startMasternode(fRestart = false) { export async function destroyMasternode() { const database = await Database.getInstance(); - if (await database.getMasternode(masterKey)) { - database.removeMasternode(masterKey); + if (await database.getMasternode(wallet.getMasterKey())) { + database.removeMasternode(wallet.getMasterKey()); createAlert('success', ALERTS.MN_DESTROYED, 5000); updateMasternodeTab(); } @@ -1432,7 +1431,7 @@ export async function importMasternode() { doms.domMnIP.value = ''; doms.domMnPrivateKey.value = ''; - if (!masterKey.isHD) { + if (!wallet.isHD()) { // Find the first UTXO matching the expected collateral size const cCollaUTXO = mempool .getConfirmed() @@ -1584,10 +1583,10 @@ export async function guiImportWallet() { fSavePublicKey: true, }); - if (masterKey) { + if (wallet.isLoaded()) { // Prepare a new Account to add const cAccount = new Account({ - publicKey: await masterKey.keyToExport, + publicKey: await wallet.getMasterKey().keyToExport, encWif: strPrivKey, }); @@ -1635,7 +1634,7 @@ export async function guiEncryptWallet() { } // Encrypt the wallet using the new password - await encryptWallet(strPass); + await wallet.encryptWallet(strPass); createAlert('success', ALERTS.NEW_PASSWORD_SUCCESS, 5500); // Hide and reset the encryption modal @@ -1644,8 +1643,8 @@ export async function guiEncryptWallet() { doms.domEncryptPasswordSecond.value = ''; // Display the 'Unlock/Lock Wallet' buttons accordingly based on state - doms.domWipeWallet.hidden = masterKey.isViewOnly; - doms.domRestoreWallet.hidden = !masterKey.isViewOnly; + doms.domWipeWallet.hidden = wallet.isViewOnly(); + doms.domRestoreWallet.hidden = !wallet.isViewOnly(); // Update the encryption UI (changes to "Change Password" now) await updateEncryptionGUI(true); @@ -1675,10 +1674,11 @@ export async function toggleExportUI() { doms.domExportPrivateKey.innerHTML = encWif; exportHidden = true; } else { - if (masterKey.isViewOnly) { + if (wallet.isViewOnly()) { exportHidden = false; } else { - doms.domExportPrivateKey.innerHTML = masterKey.keyToBackup; + doms.domExportPrivateKey.innerHTML = + wallet.getMasterKey().keyToBackup; exportHidden = true; } } @@ -1837,7 +1837,7 @@ 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 wallet.getNewAddress(true, false))[0]; // Sweep the full funds amount, minus the fee, leaving no change from any sweeped UTXOs cTx.addoutput(strAddress, (nTotal - nFee) / COIN); @@ -1917,7 +1917,7 @@ export async function wipePrivateData() { html, }) ) { - masterKey.wipePrivateData(); + wallet.getMasterKey().wipePrivateData(); doms.domWipeWallet.hidden = true; if (isEncrypted) { doms.domRestoreWallet.hidden = false; @@ -2083,7 +2083,11 @@ async function waitForSubmissionBlockHeight(cProposalCache) { */ function getProposalFinalisationStatus(cPropCache) { const cNet = getNetwork(); - const nConfsLeft = cPropCache.nSubmissionHeight + 6 - cNet.cachedBlockCount; + // Confirmations left until finalisation, by network consensus + const nConfsLeft = + cPropCache.nSubmissionHeight + + cChainParams.current.proposalFeeConfirmRequirement - + cNet.cachedBlockCount; if (cPropCache.nSubmissionHeight === 0 || cNet.cachedBlockCount === 0) { return translation.proposalFinalisationConfirming; @@ -2171,6 +2175,7 @@ async function renderProposals(arrProposals, fContested) { Nays: 0, local: true, Ratio: 0, + IsEstablished: false, mpw: p, }; }) || []; @@ -2224,12 +2229,24 @@ async function renderProposals(arrProposals, fContested) { const nNetYesPercent = (nNetYes / cMasternodes.enabled) * 100; // Proposal Status calculation - const nRequiredVotes = Math.round(cMasternodes.enabled * 0.1); - const strStatus = - nNetYes >= nRequiredVotes - ? translation.proposalPassing - : translation.proposalFailing; - let strFundingStatus = translation.proposalNotFunded; + const nRequiredVotes = cMasternodes.enabled / 10; + let strStatus = ''; + let strFundingStatus = ''; + + // Proposal Status calculations + if (nNetYes < nRequiredVotes) { + // Scenario 1: Not enough votes + strStatus = translation.proposalFailing; + strFundingStatus = translation.proposalNotFunded; + } else if (!cProposal.IsEstablished) { + // Scenario 2: Enough votes, but not established + strStatus = translation.proposalFailing; + strFundingStatus = translation.proposalTooYoung; + } else { + // Scenario 3: Enough votes, and established + strStatus = translation.proposalPassing; + strFundingStatus = translation.proposalFunded; + } // Funding Status and allocation calculations if (cProposal.local) { @@ -2240,14 +2257,14 @@ async function renderProposals(arrProposals, fContested) { updateGovernanceTab ); } - const strStatus = getProposalFinalisationStatus(cPropCache); + const strLocalStatus = getProposalFinalisationStatus(cPropCache); const finalizeButton = document.createElement('button'); finalizeButton.className = 'pivx-button-small'; finalizeButton.innerHTML = ''; if ( - strStatus === translation.proposalFinalisationReady || - strStatus === translation.proposalFinalisationExpired + strLocalStatus === translation.proposalFinalisationReady || + strLocalStatus === translation.proposalFinalisationExpired ) { finalizeButton.addEventListener('click', async () => { const result = await Masternode.finalizeProposal( @@ -2320,7 +2337,7 @@ async function renderProposals(arrProposals, fContested) { domStatus.innerHTML = ` - ${strStatus}
+ ${strLocalStatus}
@@ -2329,6 +2346,7 @@ async function renderProposals(arrProposals, fContested) { } else { if (domTable.id == 'proposalsTableBody') { if ( + cProposal.IsEstablished && nNetYes >= nRequiredVotes && totalAllocatedAmount + cProposal.MonthlyPayment <= cChainParams.current.maxPayment / COIN @@ -2566,7 +2584,7 @@ export async function updateMasternodeTab() { doms.domCreateMasternode.style.display = 'none'; doms.domMnDashboard.style.display = 'none'; - if (!masterKey) { + if (!wallet.isLoaded()) { doms.domMnTextErrors.innerHTML = 'Please ' + ((await hasEncryptedWallet()) ? 'unlock' : 'import') + @@ -2598,7 +2616,7 @@ export async function updateMasternodeTab() { doms.domControlMasternode.style.display = cMasternode ? 'block' : 'none'; // first case: the wallet is not HD and it is not hardware, so in case the wallet has collateral the user can check its status and do simple stuff like voting - if (!masterKey.isHD) { + if (!wallet.isHD()) { doms.domMnAccessMasternodeText.innerHTML = doms.masternodeLegacyAccessText; doms.domMnTxId.style.display = 'none'; @@ -2654,7 +2672,7 @@ export async function updateMasternodeTab() { for (const [key] of mapCollateralAddresses) { const option = document.createElement('option'); option.value = key; - option.innerText = await masterKey.getAddress(key); + option.innerText = await wallet.getMasterKey().getAddress(key); doms.domMnTxId.appendChild(option); } } @@ -2698,7 +2716,7 @@ async function refreshMasternodeData(cMasternode, fAlert = false) { if (cMasternodeData.status === 'MISSING') { doms.domMnTextErrors.innerHTML = 'Masternode is currently OFFLINE'; - if (!masterKey.isViewOnly) { + if (!wallet.isViewOnly()) { createAlert('warning', ALERTS.MN_OFFLINE_STARTING, 6000); // try to start the masternode const started = await cMasternode.start(); @@ -2749,7 +2767,7 @@ async function refreshMasternodeData(cMasternode, fAlert = false) { export async function createProposal() { // Must have a wallet - if (!masterKey) { + if (!wallet.isLoaded()) { return createAlert('warning', ALERTS.PROPOSAL_IMPORT_FIRST, 4500); } // Wallet must be encrypted @@ -2764,7 +2782,7 @@ export async function createProposal() { } // Wallet must be unlocked if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet(translation.walletUnlockProposal)) ) { return; @@ -2819,7 +2837,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 getNewAddress())[0]; + (await wallet.getNewAddress())[0]; const nextSuperblock = await Masternode.getNextSuperblock(); const proposal = { name: strTitle, @@ -2868,7 +2886,7 @@ export function refreshChainData() { return console.warn( 'Offline mode active: For your security, the wallet will avoid ALL internet requests.' ); - if (!masterKey) return; + if (!wallet.isLoaded()) return; // Fetch block count + UTXOs, update the UI for new transactions cNet.getBlockCount().then((_) => { diff --git a/scripts/i18n.js b/scripts/i18n.js index af7c1ff24..fcb65ee08 100644 --- a/scripts/i18n.js +++ b/scripts/i18n.js @@ -11,7 +11,7 @@ import { de_translation } from '../locale/de/translation.js'; import { Database } from './database.js'; import { fillAnalyticSelect, setTranslation } from './settings.js'; import { renderActivityGUI, updateEncryptionGUI } from './global.js'; -import { masterKey } from './wallet.js'; +import { wallet } from './wallet.js'; import { getNetwork } from './network.js'; import { cReceiveType, guiToggleReceiveType } from './contacts-book.js'; @@ -63,13 +63,13 @@ export function switchTranslation(langName) { // Translate any dynamic elements necessary const cNet = getNetwork(); - if (masterKey && cNet) { + if (wallet.isLoaded() && cNet) { updateEncryptionGUI(); renderActivityGUI(cNet.arrTxHistory); } loadAlerts(); fillAnalyticSelect(); - if (masterKey) { + if (wallet.isLoaded()) { guiToggleReceiveType(cReceiveType); } return true; diff --git a/scripts/index.js b/scripts/index.js index 6610efcee..9673cdc59 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -45,7 +45,7 @@ export { updateActivityGUI, govVote, } from './global.js'; -export { generateWallet, getNewAddress, importWallet } from './wallet.js'; +export { wallet, generateWallet, importWallet } from './wallet.js'; export { toggleTestnet, toggleDebug, diff --git a/scripts/ledger.js b/scripts/ledger.js new file mode 100644 index 000000000..f64b390a5 --- /dev/null +++ b/scripts/ledger.js @@ -0,0 +1,135 @@ +import createXpub from 'create-xpub'; +import { ALERTS, tr } from './i18n.js'; +import AppBtc from '@ledgerhq/hw-app-btc'; +import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; +import { createAlert, sleep } from './misc.js'; + +let transport; +export let cHardwareWallet = null; +export let strHardwareName = ''; +export async function getHardwareWalletKeys( + path, + xpub = false, + verify = false, + _attempts = 0 +) { + try { + // Check if we haven't setup a connection yet OR the previous connection disconnected + if (!cHardwareWallet || transport._disconnectEmitted) { + transport = await TransportWebUSB.create(); + cHardwareWallet = new AppBtc({ transport, currency: 'PIVX' }); + } + + // Update device info and fetch the pubkey + strHardwareName = + transport.device.manufacturerName + + ' ' + + transport.device.productName; + + // Prompt the user in both UIs + if (verify) createAlert('info', ALERTS.WALLET_CONFIRM_L, 3500); + const cPubKey = await cHardwareWallet.getWalletPublicKey(path, { + verify, + format: 'legacy', + }); + + if (xpub) { + return createXpub({ + depth: 3, + childNumber: 2147483648, + chainCode: cPubKey.chainCode, + publicKey: cPubKey.publicKey, + }); + } else { + return cPubKey.publicKey; + } + } catch (e) { + if (e.message.includes('denied by the user')) { + // User denied an operation + return false; + } + + // If there's no device, nudge the user to plug it in. + if (e.message.toLowerCase().includes('no device selected')) { + createAlert('info', ALERTS.WALLET_NO_HARDWARE, 10000); + return false; + } + + // If the device is unplugged, or connection lost through other means (such as spontanious device explosion) + if (e.message.includes("Failed to execute 'transferIn'")) { + createAlert( + 'info', + tr(ALERTS.WALLET_HARDWARE_CONNECTION_LOST, [ + { + hardwareWallet: strHardwareName, + }, + ]), + 10000 + ); + return false; + } + if (_attempts < 10) { + // This is an ugly hack :( + // in the event where multiple parts of the code decide to ask for an address, just + // Retry at most 10 times waiting 200ms each time + await sleep(200); + return await getHardwareWalletKeys( + path, + xpub, + verify, + _attempts + 1 + ); + } + + // If the ledger is busy, just nudge the user. + if (e.message.includes('is busy')) { + createAlert( + 'info', + tr(ALERTS.WALLET_HARDWARE_BUSY, [ + { + hardwareWallet: strHardwareName, + }, + ]), + 7500 + ); + return false; + } + + // Check if this is an expected error + if (!e.statusCode || !LEDGER_ERRS.has(e.statusCode)) { + console.error( + 'MISSING LEDGER ERROR-CODE TRANSLATION! - Please report this below error on our GitHub so we can handle it more nicely!' + ); + console.error(e); + } + + // Translate the error to a user-friendly string (if possible) + createAlert( + 'warning', + tr(ALERTS.WALLET_HARDWARE_ERROR, [ + { + hardwareWallet: strHardwareName, + }, + { + error: LEDGER_ERRS.get(e.statusCode), + }, + ]), + 5500 + ); + + return false; + } +} + +// Ledger Hardware wallet constants +export const LEDGER_ERRS = new Map([ + // Ledger error code <--> User-friendly string + [25870, 'Open the PIVX app on your device'], + [25873, 'Open the PIVX app on your device'], + [57408, 'Navigate to the PIVX app on your device'], + [27157, 'Wrong app! Open the PIVX app on your device'], + [27266, 'Wrong app! Open the PIVX app on your device'], + [27904, 'Wrong app! Open the PIVX app on your device'], + [27010, 'Unlock your Ledger, then try again!'], + [27404, 'Unlock your Ledger, then try again!'], +]); diff --git a/scripts/masterkey.js b/scripts/masterkey.js new file mode 100644 index 000000000..c6bda516a --- /dev/null +++ b/scripts/masterkey.js @@ -0,0 +1,338 @@ +import HDKey from 'hdkey'; +import { getNetwork } from './network.js'; +import { bytesToHex } from './utils.js'; +import { getHardwareWalletKeys } from './ledger.js'; +import { cChainParams, MAX_ACCOUNT_GAP } from './chain_params.js'; + +import { deriveAddress, generateOrEncodePrivkey } from './encoding.js'; + +/** + * Abstract class masterkey + * @abstract + */ +export class MasterKey { + #addressIndex = 0; + /** + * Map our own address -> Path + * @type {Map} + */ + #ownAddresses = new Map(); + + constructor() { + if (this.constructor === MasterKey) { + throw new Error('initializing virtual class'); + } + } + + /** + * @param {String} [path] - BIP32 path pointing to the private key. + * @return {Promise>} Array of bytes containing private key + * @abstract + */ + async getPrivateKeyBytes(_path) { + throw new Error('Not implemented'); + } + + /** + * @param {String} [path] - BIP32 path pointing to the private key. + * @return {Promise} encoded private key + * @abstract + */ + async getPrivateKey(path) { + return generateOrEncodePrivkey(await this.getPrivateKeyBytes(path)) + .strWIF; + } + + /** + * @param {String} [path] - BIP32 path pointing to the address + * @return {Promise} Address + * @abstract + */ + async getAddress(path) { + return deriveAddress({ pkBytes: await this.getPrivateKeyBytes(path) }); + } + + /** + * @param {String} path - BIP32 path pointing to the xpub + * @return {Promise} xpub + * @abstract + */ + async getxpub(_path) { + throw new Error('Not implemented'); + } + + /** + * Wipe all private data from key. + * @return {void} + * @abstract + */ + wipePrivateData() { + throw new Error('Not implemented'); + } + + /** + * @return {String} private key suitable for backup. + * @abstract + */ + get keyToBackup() { + throw new Error('Not implemented'); + } + + /** + * @return {Promise} public key to export. Only suitable for monitoring balance. + * @abstract + */ + get keyToExport() { + throw new Error('Not implemented'); + } + + /** + * @return {Boolean} Whether or not this is a Hierarchical Deterministic wallet + */ + get isHD() { + return this._isHD; + } + + /** + * @return {Boolean} Whether or not this is a hardware wallet + */ + get isHardwareWallet() { + return this._isHardwareWallet; + } + + /** + * @return {Boolean} Whether or not this key is view only or not + */ + get isViewOnly() { + return this._isViewOnly; + } + + // Construct a full BIP44 pubkey derivation path from it's parts + getDerivationPath(nAccount = 0, nReceiving = 0, nIndex = 0) { + // Coin-Type is different on Ledger, as such, for local wallets; we modify it if we're using a Ledger to derive a key + const strCoinType = this.isHardwareWallet + ? cChainParams.current.BIP44_TYPE_LEDGER + : cChainParams.current.BIP44_TYPE; + if (!this.isHD && !this.isHardwareWallet) { + return `:)//${strCoinType}'`; + } + return `m/44'/${strCoinType}'/${nAccount}'/${nReceiving}/${nIndex}`; + } + + /** + * @param {string} address - address to check + * @return {Promise} BIP32 path or null if it's not your address + */ + async isOwnAddress(address) { + if (this.#ownAddresses.has(address)) { + return this.#ownAddresses.get(address); + } + const last = getNetwork().lastWallet; + this.#addressIndex = + this.#addressIndex > last ? this.#addressIndex : last; + if (this.isHD) { + for (let i = 0; i < this.#addressIndex; i++) { + const path = this.getDerivationPath(0, 0, i); + const testAddress = await this.getAddress(path); + if (address === testAddress) { + this.#ownAddresses.set(address, path); + return path; + } + } + } else { + const value = address === (await this.keyToExport) ? ':)' : null; + this.#ownAddresses.set(address, value); + return value; + } + + this.#ownAddresses.set(address, null); + return null; + } + + /** + * @return Promise<[string, string]> Address and its BIP32 derivation path + */ + async getNewAddress() { + const last = getNetwork().lastWallet; + this.#addressIndex = + (this.#addressIndex > last ? this.#addressIndex : last) + 1; + if (this.#addressIndex - 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; + } + const path = this.getDerivationPath(0, 0, this.#addressIndex); + const address = await this.getAddress(path); + return [address, path]; + } + + /** + * Derive the current address (by internal index) + * @return {Promise} Address + * @abstract + */ + async getCurrentAddress() { + return await this.getAddress( + this.getDerivationPath(0, 0, this.#addressIndex) + ); + } +} + +export class HdMasterKey extends MasterKey { + constructor({ seed, xpriv, xpub }) { + super(); + // Generate the HDKey + if (seed) this._hdKey = HDKey.fromMasterSeed(seed); + if (xpriv) this._hdKey = HDKey.fromExtendedKey(xpriv); + if (xpub) this._hdKey = HDKey.fromExtendedKey(xpub); + this._isViewOnly = !!xpub; + if (!this._hdKey) + throw new Error('All of seed, xpriv and xpub are undefined'); + this._isHD = true; + this._isHardwareWallet = false; + } + + async getPrivateKeyBytes(path) { + if (this.isViewOnly) { + throw new Error( + 'Trying to get private key bytes from a view only key' + ); + } + return this._hdKey.derive(path).privateKey; + } + + get keyToBackup() { + if (this.isViewOnly) { + throw new Error('Trying to get private key from a view only key'); + } + return this._hdKey.privateExtendedKey; + } + + async getxpub(path) { + if (this.isViewOnly) return this._hdKey.publicExtendedKey; + return this._hdKey.derive(path).publicExtendedKey; + } + + getAddress(path) { + let child; + if (this.isViewOnly) { + // If we're view only we can't derive hardened keys, so we'll assume + // That the xpub has already been derived + child = this._hdKey.derive( + path + .split('/') + .filter((n) => !n.includes("'")) + .join('/') + ); + } else { + child = this._hdKey.derive(path); + } + return deriveAddress({ publicKey: bytesToHex(child.publicKey) }); + } + + wipePrivateData() { + if (this._isViewOnly) return; + + this._hdKey = HDKey.fromExtendedKey(this.keyToExport); + this._isViewOnly = true; + } + + get keyToExport() { + if (this._isViewOnly) return this._hdKey.publicExtendedKey; + // We need the xpub to point at the account level + return this._hdKey.derive( + this.getDerivationPath(0, 0, 0).split('/').slice(0, 4).join('/') + ).publicExtendedKey; + } +} + +export class HardwareWalletMasterKey extends MasterKey { + constructor() { + super(); + this._isHD = true; + this._isHardwareWallet = true; + } + async getPrivateKeyBytes(_path) { + throw new Error('Hardware wallets cannot export private keys'); + } + + async getAddress(path, { verify } = {}) { + return deriveAddress({ + publicKey: await this.getPublicKey(path, { verify }), + }); + } + + async getPublicKey(path, { verify } = {}) { + return deriveAddress({ + publicKey: await getHardwareWalletKeys(path, false, verify), + output: 'COMPRESSED_HEX', + }); + } + + get keyToBackup() { + throw new Error("Hardware wallets don't have keys to backup"); + } + + async getxpub(path) { + if (!this.xpub) { + this.xpub = await getHardwareWalletKeys(path, true); + } + return this.xpub; + } + + // Hardware Wallets don't have exposed private data + wipePrivateData() {} + + get isViewOnly() { + return false; + } + get keyToExport() { + const derivationPath = this.getDerivationPath() + .split('/') + .slice(0, 4) + .join('/'); + return this.getxpub(derivationPath); + } +} + +export class LegacyMasterKey extends MasterKey { + constructor({ pkBytes, address }) { + super(); + this._isHD = false; + this._isHardwareWallet = false; + this._pkBytes = pkBytes; + this._address = address || super.getAddress(); + this._isViewOnly = !!address; + } + + getAddress() { + return this._address; + } + + get keyToExport() { + return this._address; + } + + async getPrivateKeyBytes(_path) { + if (this.isViewOnly) { + throw new Error( + 'Trying to get private key bytes from a view only key' + ); + } + return this._pkBytes; + } + + get keyToBackup() { + return generateOrEncodePrivkey(this._pkBytes).strWIF; + } + + async getxpub(_path) { + throw new Error( + 'Trying to get an extended public key from a legacy address' + ); + } + + wipePrivateData() { + this._pkBytes = null; + this._isViewOnly = true; + } +} diff --git a/scripts/masternode.js b/scripts/masternode.js index 42bf9b329..75d769c60 100644 --- a/scripts/masternode.js +++ b/scripts/masternode.js @@ -1,11 +1,8 @@ import { cNode, cExplorer } from './settings.js'; import { cChainParams, COIN } from './chain_params.js'; -import { - masterKey, - parseWIF, - deriveAddress, - cHardwareWallet, -} from './wallet.js'; +import { wallet } from './wallet.js'; +import { parseWIF, deriveAddress } from './encoding.js'; +import { cHardwareWallet } from './ledger.js'; import { dSHA256, bytesToHex, hexToBytes } from './utils.js'; import { Buffer } from 'buffer'; import { Address6 } from 'ip-address'; @@ -43,7 +40,9 @@ export default class Masternode { static sessionVotes = []; async _getWalletPrivateKey() { - return await masterKey.getPrivateKey(this.walletPrivateKeyPath); + return await wallet + .getMasterKey() + .getPrivateKey(this.walletPrivateKeyPath); } /** @@ -206,7 +205,7 @@ export default class Masternode { sigTime, }); - if (masterKey.isHardwareWallet) { + if (wallet.isHardwareWallet()) { const { r, s, v } = await cHardwareWallet.signMessage( this.walletPrivateKeyPath, bytesToHex(toSign) @@ -251,9 +250,11 @@ export default class Masternode { } async getWalletPublicKey() { - if (masterKey.isHardwareWallet) { + if (wallet.isHardwareWallet()) { return hexToBytes( - await masterKey.getPublicKey(this.walletPrivateKeyPath) + await wallet + .getMasterKey() + .getPublicKey(this.walletPrivateKeyPath) ); } else { const walletPrivateKey = await this._getWalletPrivateKey(); diff --git a/scripts/network.js b/scripts/network.js index bec233e94..c7fd4d8dc 100644 --- a/scripts/network.js +++ b/scripts/network.js @@ -1,4 +1,3 @@ -import { getDerivationPath } from './wallet.js'; import { cChainParams, COIN } from './chain_params.js'; import { createAlert } from './misc.js'; import { Mempool, UTXO } from './mempool.js'; @@ -255,9 +254,8 @@ export class ExplorerNetwork extends Network { let publicKey; // Derive our XPub, or fetch a single pubkey if (this.masterKey.isHD && !strAddress) { - const derivationPath = getDerivationPath( - this.masterKey.isHardwareWallet - ) + const derivationPath = this.masterKey + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); @@ -382,9 +380,8 @@ export class ExplorerNetwork extends Network { // Form the API call using our wallet information const fHD = this.masterKey.isHD; - const strDerivPath = getDerivationPath( - this.masterKey.isHardwareWallet - ) + const strDerivPath = this.masterKey + .getDerivationPath() .split('/') .slice(0, 4) .join('/'); diff --git a/scripts/promos.js b/scripts/promos.js index 2fa895359..4630387f8 100644 --- a/scripts/promos.js +++ b/scripts/promos.js @@ -11,7 +11,9 @@ import { ALERTS, translation, tr } from './i18n'; import { getNetwork } from './network'; import { scanQRCode } from './scanner'; import { createAndSendTransaction } from './transactions'; -import { deriveAddress, LegacyMasterKey, masterKey } from './wallet'; +import { wallet } from './wallet'; +import { LegacyMasterKey } from './masterkey'; +import { deriveAddress } from './encoding'; /** The fee in Sats to use for Creating or Redeeming PIVX Promos */ export const PROMO_FEE = 10000; @@ -456,7 +458,7 @@ export async function updatePromoCreationTick(fRecursive = false) { const strAddress = deriveAddress({ pkBytes: cThread.thread.key }); // Ensure the wallet is unlocked - if (masterKey.isViewOnly) { + if (wallet.isViewOnly()) { $('#redeemCodeModal').modal('hide'); if (await restoreWallet(translation.walletUnlockPromo)) { // Unlocked! Re-show the promo UI and continue @@ -469,7 +471,7 @@ export async function updatePromoCreationTick(fRecursive = false) { } // Send the fill transaction if unlocked - if (!masterKey.isViewOnly) { + if (!wallet.isViewOnly()) { const res = await createAndSendTransaction({ address: strAddress, amount: cThread.amount * COIN + 10000, diff --git a/scripts/settings.js b/scripts/settings.js index 9beaf8bc7..08a5db7ce 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -11,12 +11,7 @@ import { updateEncryptionGUI, updateGovernanceTab, } from './global.js'; -import { - hasEncryptedWallet, - importWallet, - masterKey, - setMasterKey, -} from './wallet.js'; +import { wallet, hasEncryptedWallet, importWallet } from './wallet.js'; import { cChainParams } from './chain_params.js'; import { setNetwork, ExplorerNetwork, getNetwork } from './network.js'; import { confirmPopup, createAlert, isEmpty } from './misc.js'; @@ -278,7 +273,7 @@ export async function setExplorer(explorer, fSilent = false) { cExplorer = explorer; // Enable networking + notify if allowed - const network = new ExplorerNetwork(cExplorer.url, masterKey); + const network = new ExplorerNetwork(cExplorer.url, wallet.getMasterKey()); setNetwork(network); // Update the selector UI @@ -469,7 +464,7 @@ export async function toggleTestnet() { : cChainParams.testnet; // If the current wallet is not saved, we'll ask the user for confirmation, since they'll lose their wallet if they switch with an unsaved wallet! - if (masterKey && !(await hasEncryptedWallet())) { + if (wallet.isLoaded() && !(await hasEncryptedWallet())) { const fContinue = await confirmPopup({ title: tr(translation.netSwitchUnsavedWarningTitle, [ { network: cChainParams.current.name }, @@ -520,7 +515,7 @@ export async function toggleTestnet() { await importWallet({ newWif: cNewAccount.publicKey }); } else { // Nuke the Master Key - setMasterKey(null); + wallet.setMasterKey(null); // Hide all Dashboard info, kick the user back to the "Getting Started" area doms.domGenKeyWarning.style.display = 'none'; @@ -551,7 +546,7 @@ export async function toggleTestnet() { mempool.UTXOs = []; getBalance(true); getStakingBalance(true); - await updateEncryptionGUI(!!masterKey); + await updateEncryptionGUI(!!wallet.getMasterKey()); await fillExplorerSelect(); await fillNodeSelect(); await updateActivityGUI(); diff --git a/scripts/transactions.js b/scripts/transactions.js index 2b87c8ebd..bf9bb86f1 100644 --- a/scripts/transactions.js +++ b/scripts/transactions.js @@ -10,15 +10,9 @@ import { toggleBottomMenu, guiSetColdStakingAddress, } from './global.js'; -import { - hasWalletUnlocked, - masterKey, - getNewAddress, - cHardwareWallet, - strHardwareName, - getDerivationPath, - HdMasterKey, -} from './wallet.js'; +import { cHardwareWallet, strHardwareName } from './ledger.js'; +import { wallet } from './wallet.js'; +import { HdMasterKey } from './masterkey.js'; import { Mempool, UTXO } from './mempool.js'; import { getNetwork } from './network.js'; import { cChainParams, COIN, COIN_DECIMALS } from './chain_params.js'; @@ -68,11 +62,11 @@ function validateAmount(nAmountSats, nMinSats = 10000) { */ export async function createTxGUI() { // Ensure a wallet is loaded - if (!(await hasWalletUnlocked(true))) return; + if (!(await wallet.hasWalletUnlocked(true))) return; // Ensure the wallet is unlocked if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet(translation.walletUnlockTx)) ) return; @@ -110,10 +104,10 @@ export async function createTxGUI() { // Use the latest index plus one (or if the XPub is unused, then the second address) const nIndex = (cXPub.usedTokens || 0) + 1; - const strPath = getDerivationPath(false, 0, 0, nIndex, false); // Create a receiver master-key const cReceiverWallet = new HdMasterKey({ xpub: strReceiverAddress }); + const strPath = cReceiverWallet.getDerivationPath(0, 0, nIndex); // Set the 'receiver address' as the unused XPub-derived address strReceiverAddress = cReceiverWallet.getAddress(strPath); @@ -176,7 +170,7 @@ export async function createTxGUI() { export async function delegateGUI() { // Ensure the wallet is unlocked if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet( `${translation.walletUnlockStake} ${cChainParams.current.TICKER}!` )) @@ -217,13 +211,13 @@ export async function delegateGUI() { * Create a Cold Staking undelegation transaction */ export async function undelegateGUI() { - if (masterKey.isHardwareWallet) { + if (wallet.isHardwareWallet()) { return createAlert('warning', ALERTS.STAKING_LEDGER_NO_SUPPORT, 6000); } // Ensure the wallet is unlocked if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet( `${translation.walletUnlockUnstake} ${cChainParams.current.TICKER}!` )) @@ -237,7 +231,7 @@ export async function undelegateGUI() { if (!validateAmount(nAmount)) return; // Generate a new address to undelegate towards - const [address] = await getNewAddress(); + const [address] = await wallet.getNewAddress(); // Perform the TX const cTxRes = await createAndSendTransaction({ @@ -282,14 +276,14 @@ export async function createAndSendTransaction({ changeDelegationAddress = null, isProposal = false, }) { - if (!(await hasWalletUnlocked(true))) return; - if ((isDelegation || useDelegatedInputs) && masterKey.isHardwareWallet) { + if (!(await wallet.hasWalletUnlocked(true))) return; + if ((isDelegation || useDelegatedInputs) && wallet.isHardwareWallet()) { return createAlert('warning', ALERTS.STAKING_LEDGER_NO_SUPPORT, 6000); } // Ensure the wallet is unlocked if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet(translation.walletUnlockTx)) ) return; @@ -305,8 +299,8 @@ export async function createAndSendTransaction({ // Compute change (or lack thereof) const nChange = cCoinControl.nValue - (nFee + amount); - const [changeAddress, changeAddressPath] = await getNewAddress({ - verify: masterKey.isHardwareWallet, + const [changeAddress, changeAddressPath] = await wallet.getNewAddress({ + verify: wallet.isHardwareWallet(), }); /** @@ -355,7 +349,8 @@ export async function createAndSendTransaction({ // Primary output (receiver) if (isDelegation) { - const [primaryAddress, primaryAddressPath] = await getNewAddress(); + const [primaryAddress, primaryAddressPath] = + await wallet.getNewAddress(); cTx.addcoldstakingoutput(primaryAddress, address, amount / COIN); outputs.push([primaryAddress, address, amount / COIN]); @@ -394,7 +389,7 @@ export async function createAndSendTransaction({ `); } - const sign = await signTransaction(cTx, masterKey, outputs, delegateChange); + const sign = await signTransaction(cTx, wallet, outputs, delegateChange); const result = await getNetwork().sendTransaction(sign); // Update the mempool if (result) { @@ -415,7 +410,7 @@ export async function createAndSendTransaction({ } if (!isDelegation && !isProposal) { - const path = await masterKey.isOwnAddress(address); + const path = await wallet.isOwnAddress(address); // If the tx was sent to yourself, add it to the mempool if (path) { @@ -442,13 +437,13 @@ export async function createAndSendTransaction({ export async function createMasternode() { // Ensure the wallet is unlocked if ( - masterKey.isViewOnly && + wallet.isViewOnly() && !(await restoreWallet(translation.walletUnlockCreateMN)) ) return; // Generate the Masternode collateral - const [address] = await getNewAddress(); + const [address] = await wallet.getNewAddress(); const result = await createAndSendTransaction({ amount: cChainParams.current.collateralInSats, address, @@ -473,10 +468,10 @@ export async function createMasternode() { database.removeMasternode(); } -export async function signTransaction(cTx, masterKey, outputs, undelegate) { - if (!masterKey.isHardwareWallet) { +export async function signTransaction(cTx, wallet, outputs, undelegate) { + if (!wallet.isHardwareWallet()) { return await cTx.sign( - masterKey, + wallet.getMasterKey(), 1, undelegate ? 'coldstake' : undefined ); diff --git a/scripts/vanitygen_worker.js b/scripts/vanitygen_worker.js index 7255ed531..fa7057dcd 100644 --- a/scripts/vanitygen_worker.js +++ b/scripts/vanitygen_worker.js @@ -1,4 +1,4 @@ -import { deriveAddress } from './wallet.js'; +import { deriveAddress } from './encoding'; import { getSafeRand } from './misc.js'; onmessage = function (_evt) { diff --git a/scripts/wallet.js b/scripts/wallet.js index fc91fbe76..5163fa4c0 100644 --- a/scripts/wallet.js +++ b/scripts/wallet.js @@ -1,19 +1,17 @@ -import { hexToBytes, bytesToHex, dSHA256 } from './utils.js'; -import * as nobleSecp256k1 from '@noble/secp256k1'; -import { sha256 } from '@noble/hashes/sha256'; -import { ripemd160 } from '@noble/hashes/ripemd160'; +import { parseWIF } from './encoding.js'; import { generateMnemonic, mnemonicToSeed, validateMnemonic } from 'bip39'; import { doms, beforeUnloadListener } from './global.js'; -import HDKey from 'hdkey'; import { getNetwork } from './network.js'; import { - pubKeyHashNetworkLen, + MasterKey, + LegacyMasterKey, + HdMasterKey, + HardwareWalletMasterKey, +} from './masterkey'; +import { generateOrEncodePrivkey } from './encoding.js'; +import { confirmPopup, - writeToUint8, - pubPrebaseLen, createAlert, - sleep, - getSafeRand, isXPub, isStandardAddress, } from './misc.js'; @@ -23,574 +21,145 @@ import { getBalance, getStakingBalance, } from './global.js'; -import { - cChainParams, - MAX_ACCOUNT_GAP, - PRIVKEY_BYTE_LENGTH, -} from './chain_params.js'; import { ALERTS, tr, translation } from './i18n.js'; import { encrypt, decrypt } from './aes-gcm.js'; -import bs58 from 'bs58'; -import AppBtc from '@ledgerhq/hw-app-btc'; -import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; -import createXpub from 'create-xpub'; import * as jdenticon from 'jdenticon'; import { Database } from './database.js'; import { guiRenderCurrentReceiveModal } from './contacts-book.js'; import { Account } from './accounts.js'; import { debug, fAdvancedMode } from './settings.js'; - +import { strHardwareName, getHardwareWalletKeys } from './ledger.js'; export let fWalletLoaded = false; -/** - * Abstract class masterkey - * @abstract - */ -class MasterKey { - #addressIndex = 0; - /** - * Map our own address -> Path - * @type {Map} - */ - #ownAddresses = new Map(); - - constructor() { - if (this.constructor === MasterKey) { - throw new Error('initializing virtual class'); - } - } - +class Wallet { /** - * @param {String} [path] - BIP32 path pointing to the private key. - * @return {Promise>} Array of bytes containing private key - * @abstract + * @type {MasterKey} */ - async getPrivateKeyBytes(_path) { - throw new Error('Not implemented'); - } + #masterKey; + constructor() {} - /** - * @param {String} [path] - BIP32 path pointing to the private key. - * @return {Promise} encoded private key - * @abstract - */ - async getPrivateKey(path) { - return generateOrEncodePrivkey(await this.getPrivateKeyBytes(path)) - .strWIF; + getMasterKey() { + return this.#masterKey; } - /** - * @param {String} [path] - BIP32 path pointing to the address - * @return {Promise} Address - * @abstract - */ - async getAddress(path) { - return deriveAddress({ pkBytes: await this.getPrivateKeyBytes(path) }); + isViewOnly() { + if (!this.#masterKey) return false; + return this.#masterKey.isViewOnly; } - - /** - * @param {String} path - BIP32 path pointing to the xpub - * @return {Promise} xpub - * @abstract - */ - async getxpub(_path) { - throw new Error('Not implemented'); - } - - /** - * Wipe all private data from key. - * @return {void} - * @abstract - */ - wipePrivateData() { - throw new Error('Not implemented'); - } - - /** - * @return {String} private key suitable for backup. - * @abstract - */ - get keyToBackup() { - throw new Error('Not implemented'); - } - - /** - * @return {Promise} public key to export. Only suitable for monitoring balance. - * @abstract - */ - get keyToExport() { - throw new Error('Not implemented'); + isHD() { + if (!this.#masterKey) return false; + return this.#masterKey.isHD; } - /** - * @return {Boolean} Whether or not this is a Hierarchical Deterministic wallet - */ - get isHD() { - return this._isHD; - } - - /** - * @return {Boolean} Whether or not this is a hardware wallet - */ - get isHardwareWallet() { - return this._isHardwareWallet; - } - - /** - * @return {Boolean} Whether or not this key is view only or not - */ - get isViewOnly() { - return this._isViewOnly; - } - - /** - * @param {string} address - address to check - * @return {Promise} BIP32 path or null if it's not your address - */ - async isOwnAddress(address) { - if (this.#ownAddresses.has(address)) { - return this.#ownAddresses.get(address); - } - const last = getNetwork().lastWallet; - this.#addressIndex = - this.#addressIndex > last ? this.#addressIndex : last; - if (this.isHD) { - for (let i = 0; i < this.#addressIndex; i++) { - const path = getDerivationPath(this.isHardwareWallet, 0, 0, i); - const testAddress = await masterKey.getAddress(path); - if (address === testAddress) { - this.#ownAddresses.set(address, path); - return path; - } - } + async hasWalletUnlocked(fIncludeNetwork = false) { + if (fIncludeNetwork && !getNetwork().enabled) + return createAlert( + 'warning', + ALERTS.WALLET_OFFLINE_AUTOMATIC, + 5500 + ); + if (!this.isLoaded()) { + return createAlert( + 'warning', + tr(ALERTS.WALLET_UNLOCK_IMPORT, [ + { + unlock: (await hasEncryptedWallet()) + ? 'unlock ' + : 'import/create', + }, + ]), + 3500 + ); } else { - const value = address === (await this.keyToExport) ? ':)' : null; - this.#ownAddresses.set(address, value); - return value; + return true; } - - this.#ownAddresses.set(address, null); - return null; } /** - * @return Promise<[string, string]> Address and its BIP32 derivation path + * Set or replace the active Master Key with a new Master Key + * @param {Promise} mk - The new Master Key to set active */ - async getNewAddress() { - const last = getNetwork().lastWallet; - this.#addressIndex = - (this.#addressIndex > last ? this.#addressIndex : last) + 1; - if (this.#addressIndex - 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; - } - const path = getDerivationPath( - this.isHardwareWallet, - 0, - 0, - this.#addressIndex - ); - const address = await this.getAddress(path); - return [address, path]; + async setMasterKey(mk) { + this.#masterKey = mk; + // Update the network master key + await getNetwork().setMasterKey(this.#masterKey); } - /** - * Derive the current address (by internal index) - * @return {Promise} Address - * @abstract - */ - async getCurrentAddress() { - return await this.getAddress( - getDerivationPath(this.isHardwareWallet, 0, 0, this.#addressIndex) - ); + getDefaultAddress() { + return this.#masterKey.getAddress(this.#masterKey.getDerivationPath()); } -} -export class HdMasterKey extends MasterKey { - constructor({ seed, xpriv, xpub }) { - super(); - // Generate the HDKey - if (seed) this._hdKey = HDKey.fromMasterSeed(seed); - if (xpriv) this._hdKey = HDKey.fromExtendedKey(xpriv); - if (xpub) this._hdKey = HDKey.fromExtendedKey(xpub); - this._isViewOnly = !!xpub; - if (!this._hdKey) - throw new Error('All of seed, xpriv and xpub are undefined'); - this._isHD = true; - this._isHardwareWallet = false; + isLoaded() { + return !!this.#masterKey; } - async getPrivateKeyBytes(path) { - if (this.isViewOnly) { - throw new Error( - 'Trying to get private key bytes from a view only key' - ); - } - return this._hdKey.derive(path).privateKey; - } + async encryptWallet(strPassword = '') { + // Encrypt the wallet WIF with AES-GCM and a user-chosen password - suitable for browser storage + let strEncWIF = await encrypt(this.#masterKey.keyToBackup, strPassword); + if (!strEncWIF) return false; - get keyToBackup() { - if (this.isViewOnly) { - throw new Error('Trying to get private key from a view only key'); - } - return this._hdKey.privateExtendedKey; - } + // Hide the encryption warning + doms.domGenKeyWarning.style.display = 'none'; - async getxpub(path) { - if (this.isViewOnly) return this._hdKey.publicExtendedKey; - return this._hdKey.derive(path).publicExtendedKey; - } + // Prepare to Add/Update an account in the DB + const cAccount = new Account({ + publicKey: await this.#masterKey.keyToExport, + encWif: strEncWIF, + }); - getAddress(path) { - let child; - if (this.isViewOnly) { - // If we're view only we can't derive hardened keys, so we'll assume - // That the xpub has already been derived - child = this._hdKey.derive( - path - .split('/') - .filter((n) => !n.includes("'")) - .join('/') - ); + // Incase of a "Change Password", we check if an Account already exists + const database = await Database.getInstance(); + if (await database.getAccount()) { + // Update the existing Account (new encWif) in the DB + await database.updateAccount(cAccount); } else { - child = this._hdKey.derive(path); + // Add the new Account to the DB + await database.addAccount(cAccount); } - return deriveAddress({ publicKey: bytesToHex(child.publicKey) }); - } - - wipePrivateData() { - if (this._isViewOnly) return; - - this._hdKey = HDKey.fromExtendedKey(this.keyToExport); - this._isViewOnly = true; - } - - get keyToExport() { - if (this._isViewOnly) return this._hdKey.publicExtendedKey; - // We need the xpub to point at the account level - return this._hdKey.derive( - getDerivationPath(false, 0, 0, 0, false) - .split('/') - .slice(0, 4) - .join('/') - ).publicExtendedKey; - } -} - -export class HardwareWalletMasterKey extends MasterKey { - constructor() { - super(); - this._isHD = true; - this._isHardwareWallet = true; - } - async getPrivateKeyBytes(_path) { - throw new Error('Hardware wallets cannot export private keys'); - } - - async getAddress(path, { verify } = {}) { - return deriveAddress({ - publicKey: await this.getPublicKey(path, { verify }), - }); - } - async getPublicKey(path, { verify } = {}) { - return deriveAddress({ - publicKey: await getHardwareWalletKeys(path, false, verify), - output: 'COMPRESSED_HEX', + // Remove the exit blocker, we can annoy the user less knowing the key is safe in their database! + removeEventListener('beforeunload', beforeUnloadListener, { + capture: true, }); } - - get keyToBackup() { - throw new Error("Hardware wallets don't have keys to backup"); - } - - async getxpub(path) { - if (!this.xpub) { - this.xpub = await getHardwareWalletKeys(path, true); + async getNewAddress({ updateGUI = false, verify = false } = {}) { + const [address, path] = await this.#masterKey.getNewAddress(); + if (verify && this.#masterKey.isHardwareWallet) { + // Generate address to present to the user without asking to verify + const confAddress = await confirmPopup({ + title: ALERTS.CONFIRM_POPUP_VERIFY_ADDR, + html: createAddressConfirmation(address), + resolvePromise: this.#masterKey.getAddress(path, { verify }), + }); + if (address !== confAddress) { + throw new Error('User did not verify address'); + } } - return this.xpub; - } - - // Hardware Wallets don't have exposed private data - wipePrivateData() {} - - get isViewOnly() { - return false; - } - get keyToExport() { - const derivationPath = getDerivationPath(masterKey.isHardwareWallet) - .split('/') - .slice(0, 4) - .join('/'); - return this.getxpub(derivationPath); - } -} - -export class LegacyMasterKey extends MasterKey { - constructor({ pkBytes, address }) { - super(); - this._isHD = false; - this._isHardwareWallet = false; - this._pkBytes = pkBytes; - this._address = address || super.getAddress(); - this._isViewOnly = !!address; - } - - getAddress() { - return this._address; - } - - get keyToExport() { - return this._address; - } - async getPrivateKeyBytes(_path) { - if (this.isViewOnly) { - throw new Error( - 'Trying to get private key bytes from a view only key' - ); + // If we're generating a new address manually, then render the new address in our Receive Modal + if (updateGUI) { + guiRenderCurrentReceiveModal(); } - return this._pkBytes; - } - - get keyToBackup() { - return generateOrEncodePrivkey(this._pkBytes).strWIF; - } - async getxpub(_path) { - throw new Error( - 'Trying to get an extended public key from a legacy address' - ); + return [address, path]; } - - wipePrivateData() { - this._pkBytes = null; - this._isViewOnly = true; + // If the privateKey is null then the user connected a hardware wallet + isHardwareWallet() { + if (!this.#masterKey) return false; + return this.#masterKey.isHardwareWallet == true; } -} - -// Ledger Hardware wallet constants -export const LEDGER_ERRS = new Map([ - // Ledger error code <--> User-friendly string - [25870, 'Open the PIVX app on your device'], - [25873, 'Open the PIVX app on your device'], - [57408, 'Navigate to the PIVX app on your device'], - [27157, 'Wrong app! Open the PIVX app on your device'], - [27266, 'Wrong app! Open the PIVX app on your device'], - [27904, 'Wrong app! Open the PIVX app on your device'], - [27010, 'Unlock your Ledger, then try again!'], - [27404, 'Unlock your Ledger, then try again!'], -]); - -/** - * @type{MasterKey} - */ -export let masterKey; - -// Construct a full BIP44 pubkey derivation path from it's parts -export function getDerivationPath( - fLedger = false, - nAccount = 0, - nReceiving = 0, - nIndex = 0, - /** - * When `true` will derive based on local wallet properties, when `false` it - * will default to only accept given params and ignore the local configuration - * @type {boolean} - */ - fLocalWallet = true -) { - // Coin-Type is different on Ledger, as such, for local wallets; we modify it if we're using a Ledger to derive a key - const strCoinType = - fLocalWallet && fLedger - ? cChainParams.current.BIP44_TYPE_LEDGER - : cChainParams.current.BIP44_TYPE; - if (fLocalWallet && masterKey && !masterKey.isHD && !fLedger) { - return `:)//${strCoinType}'`; + async isOwnAddress(address) { + return await this.#masterKey.isOwnAddress(address); } - return `m/44'/${strCoinType}'/${nAccount}'/${nReceiving}/${nIndex}`; -} - -// Verify the integrity of a WIF private key, optionally parsing and returning the key payload -export function verifyWIF( - strWIF = '', - fParseBytes = false, - skipVerification = false -) { - // Convert from Base58 - const bWIF = bs58.decode(strWIF); - - if (!skipVerification) { - // Verify the byte length - if (bWIF.byteLength !== PRIVKEY_BYTE_LENGTH) { - throw Error( - 'Private key length (' + - bWIF.byteLength + - ') is invalid, should be ' + - PRIVKEY_BYTE_LENGTH + - '!' - ); - } - - // Verify the network byte - if (bWIF[0] !== cChainParams.current.SECRET_KEY) { - // Find the network it's trying to use, if any - const cNetwork = Object.keys(cChainParams) - .filter((strNet) => strNet !== 'current') - .map((strNet) => cChainParams[strNet]) - .find((cNet) => cNet.SECRET_KEY === bWIF[0]); - // Give a specific alert based on the byte properties - throw Error( - cNetwork - ? 'This private key is for ' + - (cNetwork.isTestnet ? 'Testnet' : 'Mainnet') + - ', wrong network!' - : 'This private key belongs to another coin, or is corrupted.' - ); - } - - // Perform SHA256d hash of the WIF bytes - const shaHash = dSHA256(bWIF.slice(0, 34)); - - // Verify checksum (comparison by String since JS hates comparing object-like primitives) - const bChecksumWIF = bWIF.slice(bWIF.byteLength - 4); - const bChecksum = shaHash.slice(0, 4); - if (bChecksumWIF.join('') !== bChecksum.join('')) { - throw Error( - 'Private key checksum is invalid, key may be modified, mis-typed, or corrupt.' - ); - } + getDerivationPath(nAccount = 0, nReceiving = 0, nIndex = 0) { + return this.#masterKey.getDerivationPath(nAccount, nReceiving, nIndex); } - - return fParseBytes ? Uint8Array.from(bWIF.slice(1, 33)) : true; } -// A convenient alias to verifyWIF that returns the raw byte payload -export function parseWIF(strWIF, skipVerification = false) { - return verifyWIF(strWIF, true, skipVerification); -} - -/** - * Private key in Bytes and WIF formats - * @typedef {Object} PrivateKey - * @property {Uint8Array} pkBytes - The unprocessed Private Key bytes. - * @property {string} strWIF - The WIF encoded private key string. - */ - /** - * Network encode 32 bytes for a private key - * @param {Uint8Array} pkBytes - 32 Bytes - * @returns {Uint8Array} - The network-encoded Private Key bytes + * @type{Wallet} */ -export function encodePrivkeyBytes(pkBytes) { - const pkNetBytes = new Uint8Array(pkBytes.length + 2); - pkNetBytes[0] = cChainParams.current.SECRET_KEY; // Private key prefix (1 byte) - writeToUint8(pkNetBytes, pkBytes, 1); // Private key bytes (32 bytes) - pkNetBytes[pkNetBytes.length - 1] = 1; // Leading digit (1 byte) - return pkNetBytes; -} - -/** - * Generate a new private key OR encode an existing private key from raw bytes - * @param {Uint8Array} pkBytesToEncode - Bytes to encode as a coin private key - * @returns {PrivateKey} - The private key - */ -export function generateOrEncodePrivkey(pkBytesToEncode) { - // Private Key Generation - const pkBytes = pkBytesToEncode || getSafeRand(); - - // Network Encoding - const pkNetBytes = encodePrivkeyBytes(pkBytes); - - // Double SHA-256 hash - const shaObj = dSHA256(pkNetBytes); - - // WIF Checksum - const checksum = shaObj.slice(0, 4); - const keyWithChecksum = new Uint8Array(34 + checksum.length); - writeToUint8(keyWithChecksum, pkNetBytes, 0); - writeToUint8(keyWithChecksum, checksum, 34); - - // Return both the raw bytes and the WIF format - return { pkBytes, strWIF: bs58.encode(keyWithChecksum) }; -} - -/** - * Compress an uncompressed Public Key in byte form - * @param {Array | Uint8Array} pubKeyBytes - The uncompressed public key bytes - * @returns {Array} The compressed public key bytes - */ -function compressPublicKey(pubKeyBytes) { - if (pubKeyBytes.length != 65) - throw new Error('Attempting to compress an invalid uncompressed key'); - const x = pubKeyBytes.slice(1, 33); - const y = pubKeyBytes.slice(33); - - // Compressed key is [key_parity + 2, x] - return [y[31] % 2 === 0 ? 2 : 3, ...x]; -} - -/** - * Derive a Secp256k1 network-encoded public key (coin address) from raw private or public key bytes - * @param {Object} options - The object to deconstruct - * @param {String} [options.publicKey] - The hex encoded public key. Can be both compressed or uncompressed - * @param {Array | Uint8Array} [options.pkBytes] - An array of bytes containing the private key - * @param {"ENCODED" | "UNCOMPRESSED_HEX" | "COMPRESSED_HEX"} options.output - Output - * @return {String} the public key with the specified encoding - */ -export function deriveAddress({ pkBytes, publicKey, output = 'ENCODED' }) { - if (!pkBytes && !publicKey) return null; - const compress = output !== 'UNCOMPRESSED_HEX'; - // Public Key Derivation - let pubKeyBytes = publicKey - ? hexToBytes(publicKey) - : nobleSecp256k1.getPublicKey(pkBytes, compress); - - if (output === 'UNCOMPRESSED_HEX') { - if (pubKeyBytes.length !== 65) { - // It's actually possible, but it's probably not something that we'll need - throw new Error("Can't uncompress an already compressed key"); - } - return bytesToHex(pubKeyBytes); - } - - if (pubKeyBytes.length === 65) { - pubKeyBytes = compressPublicKey(pubKeyBytes); - } - - if (pubKeyBytes.length != 33) { - throw new Error('Invalid public key'); - } - - if (output === 'COMPRESSED_HEX') { - return bytesToHex(pubKeyBytes); - } - - // First pubkey SHA-256 hash - const pubKeyHashing = sha256(new Uint8Array(pubKeyBytes)); - - // RIPEMD160 hash - const pubKeyHashRipemd160 = ripemd160(pubKeyHashing); - - // Network Encoding - const pubKeyHashNetwork = new Uint8Array(pubKeyHashNetworkLen); - pubKeyHashNetwork[0] = cChainParams.current.PUBKEY_ADDRESS; - writeToUint8(pubKeyHashNetwork, pubKeyHashRipemd160, 1); - - // Double SHA-256 hash - const pubKeyHashingSF = dSHA256(pubKeyHashNetwork); - - // Checksum - const checksumPubKey = pubKeyHashingSF.slice(0, 4); - - // Public key pre-base58 - const pubKeyPreBase = new Uint8Array(pubPrebaseLen); - writeToUint8(pubKeyPreBase, pubKeyHashNetwork, 0); - writeToUint8(pubKeyPreBase, checksumPubKey, pubKeyHashNetworkLen); - - // Encode as Base58 human-readable network address - return bs58.encode(pubKeyPreBase); -} +export const wallet = new Wallet(); /** * Import a wallet (with it's private, public or encrypted data) @@ -622,14 +191,16 @@ export async function importWallet({ 7500 ); } + // Derive our hardware address and import! + await wallet.setMasterKey(new HardwareWalletMasterKey()); const publicKey = await getHardwareWalletKeys( - getDerivationPath(true) + wallet.getDerivationPath() ); // Errors are handled within the above function, so there's no need for an 'else' here, just silent ignore. - if (!publicKey) return; - - // Derive our hardware address and import! - await setMasterKey(new HardwareWalletMasterKey()); + if (!publicKey) { + await wallet.setMasterKey(null); + return; + } // Hide the 'export wallet' button, it's not relevant to hardware wallets doms.domExportWallet.hidden = true; @@ -677,7 +248,7 @@ export async function importWallet({ cPhraseValidator.phrase, passphrase ); - await setMasterKey(new HdMasterKey({ seed })); + await wallet.setMasterKey(new HdMasterKey({ seed })); } else if (cPhraseValidator.phrase.includes(' ')) { // The Phrase Validator failed, but the input contains at least one space; possibly a Seed Typo? return createAlert('warning', cPhraseValidator.msg, 5000); @@ -686,21 +257,21 @@ export async function importWallet({ try { // XPub import (HD view only) if (isXPub(privateImportValue)) { - await setMasterKey( + await wallet.setMasterKey( new HdMasterKey({ xpub: privateImportValue, }) ); // XPrv import (HD full access) } else if (privateImportValue.startsWith('xprv')) { - await setMasterKey( + await wallet.setMasterKey( new HdMasterKey({ xpriv: privateImportValue, }) ); // Pubkey import (non-HD view only) } else if (isStandardAddress(privateImportValue)) { - await setMasterKey( + await wallet.setMasterKey( new LegacyMasterKey({ address: privateImportValue, }) @@ -709,7 +280,9 @@ export async function importWallet({ } else { // Attempt to import a raw WIF private key const pkBytes = parseWIF(privateImportValue); - await setMasterKey(new LegacyMasterKey({ pkBytes })); + await wallet.setMasterKey( + new LegacyMasterKey({ pkBytes }) + ); } } catch (e) { return createAlert( @@ -725,7 +298,7 @@ export async function importWallet({ fWalletLoaded = true; // Hide wipe wallet button if there is no private key - if (masterKey.isViewOnly || masterKey.isHardwareWallet) { + if (wallet.isViewOnly() || wallet.isHardwareWallet()) { doms.domWipeWallet.hidden = true; if (await hasEncryptedWallet()) { doms.domRestoreWallet.hidden = false; @@ -733,24 +306,22 @@ export async function importWallet({ } // For non-HD wallets: hide the 'new address' button, since these are essentially single-address MPW wallets - if (!masterKey.isHD) doms.domNewAddress.style.display = 'none'; + if (!wallet.isHD()) doms.domNewAddress.style.display = 'none'; // Update the loaded address in the Dashboard - getNewAddress({ updateGUI: true }); + wallet.getNewAddress({ updateGUI: true }); // Display Text doms.domGuiWallet.style.display = 'block'; doms.domDashboard.click(); // Update identicon - doms.domIdenticon.dataset.jdenticonValue = masterKey.getAddress( - getDerivationPath() - ); + doms.domIdenticon.dataset.jdenticonValue = wallet.getDefaultAddress(); jdenticon.update('#identicon'); // Hide the encryption prompt if the user is using // a hardware wallet, or is view-only mode. - if (!(isHardwareWallet || masterKey.isViewOnly)) { + if (!(isHardwareWallet || wallet.isViewOnly())) { if ( // If the wallet was internally imported (not UI pasted), like via vanity, display the encryption prompt (((fRaw && newWif.length) || newWif) && @@ -779,16 +350,6 @@ export async function importWallet({ } } -/** - * Set or replace the active Master Key with a new Master Key - * @param {MasterKey} mk - The new Master Key to set active - */ -export async function setMasterKey(mk) { - masterKey = mk; - // Update the network master key - await getNetwork().setMasterKey(masterKey); -} - // Wallet Generation export async function generateWallet(noUI = false) { // TODO: remove `walletConfirm`, it is useless as Accounts cannot be overriden, and multi-accounts will come soon anyway @@ -803,7 +364,7 @@ export async function generateWallet(noUI = false) { const seed = await mnemonicToSeed(mnemonic, passphrase); // Prompt the user to encrypt the seed - await setMasterKey(new HdMasterKey({ seed })); + await wallet.setMasterKey(new HdMasterKey({ seed })); fWalletLoaded = true; doms.domGenKeyWarning.style.display = 'block'; @@ -817,19 +378,17 @@ export async function generateWallet(noUI = false) { setDisplayForAllWalletOptions('none'); // Update identicon - doms.domIdenticon.dataset.jdenticonValue = masterKey.getAddress( - getDerivationPath() - ); + doms.domIdenticon.dataset.jdenticonValue = wallet.getDefaultAddress(); jdenticon.update('#identicon'); - getNewAddress({ updateGUI: true }); + wallet.getNewAddress({ updateGUI: true }); // Refresh the balance UI (why? because it'll also display any 'get some funds!' alerts) getBalance(true); getStakingBalance(true); } - return masterKey; + return wallet; } /** @@ -927,36 +486,6 @@ function informUserOfMnemonic(mnemonic) { }); } -export async function encryptWallet(strPassword = '') { - // Encrypt the wallet WIF with AES-GCM and a user-chosen password - suitable for browser storage - let strEncWIF = await encrypt(masterKey.keyToBackup, strPassword); - if (!strEncWIF) return false; - - // Hide the encryption warning - doms.domGenKeyWarning.style.display = 'none'; - - // Prepare to Add/Update an account in the DB - const cAccount = new Account({ - publicKey: await masterKey.keyToExport, - encWif: strEncWIF, - }); - - // Incase of a "Change Password", we check if an Account already exists - const database = await Database.getInstance(); - if (await database.getAccount()) { - // Update the existing Account (new encWif) in the DB - await database.updateAccount(cAccount); - } else { - // Add the new Account to the DB - await database.addAccount(cAccount); - } - - // Remove the exit blocker, we can annoy the user less knowing the key is safe in their database! - removeEventListener('beforeunload', beforeUnloadListener, { - capture: true, - }); -} - export async function decryptWallet(strPassword = '') { // Check if there's any encrypted WIF available const database = await Database.getInstance(); @@ -987,175 +516,7 @@ export async function hasEncryptedWallet() { return !!account?.encWif; } -// If the privateKey is null then the user connected a hardware wallet -export function hasHardwareWallet() { - if (!masterKey) return false; - return masterKey.isHardwareWallet == true; -} - -export async function hasWalletUnlocked(fIncludeNetwork = false) { - if (fIncludeNetwork && !getNetwork().enabled) - return createAlert('warning', ALERTS.WALLET_OFFLINE_AUTOMATIC, 5500); - if (!masterKey) { - return createAlert( - 'warning', - tr(ALERTS.WALLET_UNLOCK_IMPORT, [ - { - unlock: (await hasEncryptedWallet()) - ? 'unlock ' - : 'import/create', - }, - ]), - 3500 - ); - } else { - return true; - } -} - function createAddressConfirmation(address) { return `${translation.popupHardwareAddrCheck} ${strHardwareName}.
${address}
`; } - -export async function getNewAddress({ - updateGUI = false, - verify = false, -} = {}) { - const [address, path] = await masterKey.getNewAddress(); - if (verify && masterKey.isHardwareWallet) { - // Generate address to present to the user without asking to verify - const confAddress = await confirmPopup({ - title: ALERTS.CONFIRM_POPUP_VERIFY_ADDR, - html: createAddressConfirmation(address), - resolvePromise: masterKey.getAddress(path, { verify }), - }); - if (address !== confAddress) { - throw new Error('User did not verify address'); - } - } - - // If we're generating a new address manually, then render the new address in our Receive Modal - if (updateGUI) { - guiRenderCurrentReceiveModal(); - } - - return [address, path]; -} - -export let cHardwareWallet = null; -export let strHardwareName = ''; -let transport; -async function getHardwareWalletKeys( - path, - xpub = false, - verify = false, - _attempts = 0 -) { - try { - // Check if we haven't setup a connection yet OR the previous connection disconnected - if (!cHardwareWallet || transport._disconnectEmitted) { - transport = await TransportWebUSB.create(); - cHardwareWallet = new AppBtc({ transport, currency: 'PIVX' }); - } - - // Update device info and fetch the pubkey - strHardwareName = - transport.device.manufacturerName + - ' ' + - transport.device.productName; - - // Prompt the user in both UIs - if (verify) createAlert('info', ALERTS.WALLET_CONFIRM_L, 3500); - const cPubKey = await cHardwareWallet.getWalletPublicKey(path, { - verify, - format: 'legacy', - }); - - if (xpub) { - return createXpub({ - depth: 3, - childNumber: 2147483648, - chainCode: cPubKey.chainCode, - publicKey: cPubKey.publicKey, - }); - } else { - return cPubKey.publicKey; - } - } catch (e) { - if (e.message.includes('denied by the user')) { - // User denied an operation - return false; - } - - // If there's no device, nudge the user to plug it in. - if (e.message.toLowerCase().includes('no device selected')) { - createAlert('info', ALERTS.WALLET_NO_HARDWARE, 10000); - return false; - } - - // If the device is unplugged, or connection lost through other means (such as spontanious device explosion) - if (e.message.includes("Failed to execute 'transferIn'")) { - createAlert( - 'info', - tr(ALERTS.WALLET_HARDWARE_CONNECTION_LOST, [ - { - hardwareWallet: strHardwareName, - }, - ]), - 10000 - ); - return false; - } - if (_attempts < 10) { - // This is an ugly hack :( - // in the event where multiple parts of the code decide to ask for an address, just - // Retry at most 10 times waiting 200ms each time - await sleep(200); - return await getHardwareWalletKeys( - path, - xpub, - verify, - _attempts + 1 - ); - } - - // If the ledger is busy, just nudge the user. - if (e.message.includes('is busy')) { - createAlert( - 'info', - tr(ALERTS.WALLET_HARDWARE_BUSY, [ - { - hardwareWallet: strHardwareName, - }, - ]), - 7500 - ); - return false; - } - - // Check if this is an expected error - if (!e.statusCode || !LEDGER_ERRS.has(e.statusCode)) { - console.error( - 'MISSING LEDGER ERROR-CODE TRANSLATION! - Please report this below error on our GitHub so we can handle it more nicely!' - ); - console.error(e); - } - - // Translate the error to a user-friendly string (if possible) - createAlert( - 'warning', - tr(ALERTS.WALLET_HARDWARE_ERROR, [ - { - hardwareWallet: strHardwareName, - }, - { - error: LEDGER_ERRS.get(e.statusCode), - }, - ]), - 5500 - ); - - return false; - } -}