Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] Decouple the wallet class #197

Merged
merged 10 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ <h3 class="noselect balance-title">
<a id="guiExportWalletItem" class="dropdown-item ptr" data-toggle="modal" data-target="#exportPrivateKeysModal" data-backdrop="static" data-keyboard="false" onclick="MPW.toggleExportUI()">
<i class="fas fa-key"></i> <span data-i18n="export">Export</span>
</a>
<a class="dropdown-item ptr" id="guiNewAddress" data-toggle="modal" data-target="#qrModal" onclick="MPW.getNewAddress({updateGUI: true, verify: true});">
<a class="dropdown-item ptr" id="guiNewAddress" data-toggle="modal" data-target="#qrModal" onclick="MPW.wallet.getNewAddress({updateGUI: true, verify: true});">
<i class="fas fa-sync-alt"></i> <span data-i18n="refreshAddress">Refresh address</span>
</a>
<a class="dropdown-item ptr" data-toggle="modal" data-target="#redeemCodeModal">
Expand Down
5 changes: 3 additions & 2 deletions scripts/bitTrx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,7 +31,7 @@ export default class bitjs {
index,
script,
sequence,
path = getDerivationPath(),
path = wallet.getDerivationPath(),
}) {
const o = {};
o.outpoint = { hash: txid, index: index };
Expand Down
54 changes: 30 additions & 24 deletions scripts/contacts-book.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 =
Expand Down Expand Up @@ -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 +
Expand All @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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',
Expand All @@ -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;
}
Expand Down Expand Up @@ -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',
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
}
}

Expand Down
187 changes: 187 additions & 0 deletions scripts/encoding.js
Original file line number Diff line number Diff line change
@@ -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<Number> | Uint8Array} pubKeyBytes - The uncompressed public key bytes
* @returns {Array<Number>} 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<Number> | 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);
}
Loading