Skip to content

Commit

Permalink
[Refactor] Decouple the wallet class (#197)
Browse files Browse the repository at this point in the history
* [Refactor] Decouple the wallet class

* Apply review suggestion

* Apply self review

* Apply self review 2

* Apply self review 3

* Rewrite getDerivationPath

* Add wallet.

* fix ledger and remove coment

* set null in case of error

* forgot await
  • Loading branch information
panleone authored Sep 19, 2023
1 parent ffb3b71 commit b7e9264
Show file tree
Hide file tree
Showing 16 changed files with 904 additions and 886 deletions.
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

0 comments on commit b7e9264

Please sign in to comment.