Skip to content
Open
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 packages/bitcore-cli/src/cli-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function getCommands(args: { wallet: IWallet, opts?: ICliOptions }) {
{ label: 'Import File', value: 'import-file', hint: 'Import using a file' },
],
BASIC: [
{ label: ({ token }) => `Token${token ? ` (${Utils.colorText(token, 'orange')})` : ''}`, value: 'token', hint: 'Manage the token context for this session', show: () => !wallet.isUtxo(), noCmd: true },
{ label: ({ token }) => `Token${token ? ` (${Utils.colorText(token, 'orange')})` : ''}`, value: 'token', hint: 'Manage the token context for this session', show: () => wallet.isTokenChain(), noCmd: true },
{ label: ({ ppNum }) => `Proposals${ppNum}`, value: 'txproposals', hint: 'Get pending transaction proposals' },
{ label: 'Send', value: 'transaction', hint: 'Create a transaction to send funds' },
{ label: 'Receive', value: 'address', hint: 'Get an address to receive funds to' },
Expand Down
13 changes: 7 additions & 6 deletions packages/bitcore-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ if (require.main === module) {
if (walletName === 'list') {
for (const file of fs.readdirSync(opts.dir)) {
if (file.endsWith('.json')) {
console.log(`- ${file.replace('.json', '')}`);
const walletData = JSON.parse(fs.readFileSync(path.join(opts.dir, file), 'utf8'));
console.log(` ${Utils.boldText(file.replace('.json', ''))} [${Utils.colorizeChain(walletData.creds.chain)}:${walletData.creds.network}]`);
}
}
return;
Expand All @@ -125,7 +126,7 @@ if (require.main === module) {
};

if (!wallet.client?.credentials) {
prompt.intro(`No wallet found named ${Utils.colorText(walletName, 'orange')}`);
prompt.intro(`No wallet found named ${Utils.underlineText(Utils.boldText(Utils.italicText(walletName)))}`);
const action: NewCommand | symbol = await prompt.select({
message: 'What would you like to do?',
options: [].concat(COMMANDS.NEW, COMMANDS.EXIT)
Expand Down Expand Up @@ -156,20 +157,20 @@ if (require.main === module) {
opts.exit = true;
break;
}
prompt.outro(`${Utils.colorText('✔', 'green')} Wallet ${Utils.colorText(walletName, 'orange')} created successfully!`);
!opts.exit && prompt.outro(`${Utils.colorText('✔', 'green')} Wallet ${Utils.boldText(walletName)} created successfully!`);
} else {

if (opts.status) {
prompt.intro(`Status for ${Utils.colorText(walletName, 'orange')}`);
prompt.intro(`Status for ${Utils.colorTextByChain(wallet.chain, walletName)}`);
const status = await commands.status.walletStatus({ wallet, opts });
cmdParams.status = status;
prompt.outro('Welcome to the Bitcore CLI!');
prompt.outro(Utils.boldText('Welcome to the Bitcore CLI!'));
}

let advancedActions = false;
do {
// Don't display the intro if running a specific command
!opts.command && prompt.intro(`${Utils.colorText('~~ Main Menu ~~', 'blue')} (${Utils.colorText(walletName, 'orange')})`);
!opts.command && prompt.intro(`${Utils.boldText('[ Main Menu')} - ${Utils.colorTextByChain(wallet.chain, walletName)} ${Utils.boldText(']')}`);
cmdParams.status.pendingTxps = opts.command ? [] : await wallet.client.getTxProposals({});

const dynamicCmdArgs = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function createThresholdSigWallet(
const { verbose, mnemonic } = opts;

const copayerName = await getCopayerName();
const addressType = await getAddressType({ chain, network, isMultiSig: false }); // TSS is treated as a single-sig
const addressType = await getAddressType({ chain, network, isMultiSig: false, isTss: true });
const password = await getPassword('Enter a password for the wallet:', { hidden: false });

let key;
Expand Down
13 changes: 8 additions & 5 deletions packages/bitcore-cli/src/commands/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as prompt from '@clack/prompts';
import { type Txp } from 'bitcore-wallet-client';
import { type Txp, Utils as BWCUtils } from 'bitcore-wallet-client';
import { Validation } from 'crypto-wallet-core';
import os from 'os';
import type { CommonArgs } from '../../types/cli';
Expand Down Expand Up @@ -73,13 +73,14 @@ export async function createTransaction(
throw new Error(`Unknown token "${opts.tokenAddress || opts.token}" on ${chain}:${network}`);
}
}
const nativeCurrency = (await wallet.getNativeCurrency(true)).displayCode;

if (!status) {
status = await wallet.client.getStatus({ tokenAddress: tokenObj?.contractAddress });
}

const { balance } = status;
const currency = tokenObj?.displayCode || chain.toUpperCase();
const currency = tokenObj?.displayCode || nativeCurrency;
const availableAmount = Utils.amountFromSats(chain, balance.availableAmount, tokenObj);

if (!balance.availableAmount) {
Expand All @@ -89,8 +90,7 @@ export async function createTransaction(


const to = opts.to || await prompt.text({
message: 'Enter the recipient address:',
placeholder: 'e.g. n2HRFgtoihgAhx1qAEXcdBMjoMvAx7AcDc',
message: 'Enter the recipient\'s address:',
validate: (value) => {
if (!Validation.validateAddress(chain, network, value)) {
return `Invalid address for ${chain}:${network}`;
Expand Down Expand Up @@ -121,7 +121,7 @@ export async function createTransaction(
if (isNaN(val) || val <= 0) {
return 'Please enter a valid amount greater than 0';
}
if (val > availableAmount) {
if (val > Number(availableAmount)) {
return 'You cannot send more than your balance';
}
return; // valid value
Expand Down Expand Up @@ -179,6 +179,9 @@ export async function createTransaction(
if (prompt.isCancel(customFeeRate)) {
throw new UserCancelled();
}
if (BWCUtils.isUtxoChain(chain)) {
customFeeRate = (Number(customFeeRate) * 1000).toString(); // convert to sats/KB
}
}

const txpParams = {
Expand Down
27 changes: 20 additions & 7 deletions packages/bitcore-cli/src/commands/txproposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as prompt from '@clack/prompts';
import fs from 'fs';
import os from 'os';
import type { CommonArgs } from '../../types/cli';
import { ITokenObj } from '../../types/wallet';
import { UserCancelled } from '../errors';
import { getAction, getFileName } from '../prompts';
import { Utils } from '../utils';
Expand Down Expand Up @@ -64,15 +65,27 @@ export async function getTxProposals(
} else {
const lines = [];
const chain = txp.chain || txp.coin;
const currency = chain.toUpperCase();
const feeCurrency = currency; // TODO
const network = txp.network;
let tokenObj: ITokenObj;
if (txp.tokenAddress) {
tokenObj = await wallet.getToken({ tokenAddress: txp.tokenAddress });
if (!tokenObj) {
throw new Error(`Unknown token "${txp.tokenAddress}" on ${chain}:${network}`);
}
}
const nativeCurrency = (await wallet.getNativeCurrency(true)).displayCode;
const currency = tokenObj?.displayCode || nativeCurrency;

lines.push(`Chain: ${chain.toUpperCase()}`);
lines.push(`Network: ${Utils.capitalize(txp.network)}`);
txp.tokenAddress && lines.push(`Token: ${txp.tokenAddress}`);
lines.push(`Amount: ${Utils.amountFromSats(chain, txp.amount)} ${currency}`);
lines.push(`Fee: ${Utils.amountFromSats(chain, txp.fee)} ${feeCurrency}`);
lines.push(`Total Amount: ${Utils.amountFromSats(chain, txp.amount + txp.fee)} ${currency}`);
lines.push(`Amount: ${Utils.renderAmount(currency, txp.amount, tokenObj)}`);
lines.push(`Fee: ${Utils.renderAmount(nativeCurrency, txp.fee)}`);
// lines.push(`Total Amount: ${Utils.amountFromSats(chain, txp.amount + txp.fee)} ${currency}`);
lines.push(`Total Amount: ${tokenObj
? Utils.renderAmount(currency, txp.amount, tokenObj) + ` + ${Utils.renderAmount(nativeCurrency, txp.fee)}`
: Utils.renderAmount(currency, txp.amount + txp.fee)
}`);
txp.gasPrice && lines.push(`Gas Price: ${Utils.displayFeeRate(chain, txp.gasPrice)}`);
txp.gasLimit && lines.push(`Gas Limit: ${txp.gasLimit}`);
txp.feePerKb && lines.push(`Fee Rate: ${Utils.displayFeeRate(chain, txp.feePerKb)}`);
Expand All @@ -85,9 +98,9 @@ export async function getTxProposals(
lines.push('---------------------------');
lines.push('Recipients:');
lines.push(...txp.outputs.map(o => {
return ` → ${Utils.maxLength(o.toAddress)}${o.tag ? `:${o.tag}` : ''}: ${Utils.amountFromSats(chain, o.amount)} ${currency}${o.message ? ` (${o.message})` : ''}`;
return ` → ${Utils.maxLength(o.toAddress)}${o.tag ? `:${o.tag}` : ''}: ${Utils.renderAmount(currency, o.amount)}${o.message ? ` (${o.message})` : ''}`;
}));
txp.changeAddress && lines.push(`Change Address: ${Utils.maxLength(txp.changeAddress.address)} (${txp.changeAddress.path})`);
txp.changeAddress && lines.push(` ${Utils.maxLength(txp.changeAddress.address)} (change - ${txp.changeAddress.path})`);
lines.push('---------------------------');
if (txp.actions?.length) {
lines.push('Actions:');
Expand Down
26 changes: 24 additions & 2 deletions packages/bitcore-cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export const Constants = {
yellow: '\x1b[33m%s\x1b[0m',
blue: '\x1b[34m%s\x1b[0m',
orange: '\x1b[38;5;208m%s\x1b[0m',
gold: '\x1b[38;5;214m%s\x1b[0m',
tan: '\x1b[38;5;180m%s\x1b[0m',
beige: '\x1b[38;5;223m%s\x1b[0m',
purple: '\x1b[38;5;129m%s\x1b[0m',
lightgray: '\x1b[38;5;250m%s\x1b[0m',
darkgray: '\x1b[38;5;236m%s\x1b[0m',
pink: '\x1b[38;5;213m%s\x1b[0m',
none: '\x1b[0m%s',
},
ADDRESS_TYPE: {
Expand All @@ -125,6 +132,11 @@ export const Constants = {
multiSig: {
P2WSH: 'witnessscripthash',
P2SH: 'scripthash',
},
thresholdSig: {
P2WSH: 'witnessscripthash',
P2PKH: 'pubkeyhash',
// TSS doesn't support schnorr sigs, hence no P2TR
}
},
BCH: {
Expand All @@ -133,6 +145,9 @@ export const Constants = {
},
multiSig: {
P2SH: 'scripthash'
},
thresholdSig: {
P2PKH: 'pubkeyhash',
}
},
LTC: {
Expand All @@ -143,17 +158,24 @@ export const Constants = {
multiSig: {
P2WSH: 'witnessscripthash',
P2SH: 'scripthash',
}
},
thresholdSig: {
P2WPKH: 'witnesspubkeyhash',
P2PKH: 'pubkeyhash',
}
},
DOGE: {
singleSig: {
P2PKH: 'pubkeyhash',
},
multiSig: {
P2SH: 'scripthash'
},
thresholdSig: {
P2PKH: 'pubkeyhash',
}
},
default: 'scripthash'
default: 'pubkeyhash'
}
};

Expand Down
13 changes: 11 additions & 2 deletions packages/bitcore-cli/src/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as prompt from '@clack/prompts';
import { Network } from 'bitcore-wallet-client';
import { BitcoreLib, BitcoreLibLtc } from 'crypto-wallet-core';
import { BitcoreLib, BitcoreLibLtc, Constants as CWCConst } from 'crypto-wallet-core';
import { Constants } from './constants';
import { UserCancelled } from './errors';
import { Utils } from './utils';
Expand All @@ -17,6 +17,12 @@ export async function getChain(): Promise<string> {
message: 'Chain:',
placeholder: `Default: ${defaultVal}`,
defaultValue: defaultVal,
validate: (input) => {
if (CWCConst.CHAINS.includes(input?.toLowerCase())) {
return; // valid input
}
return `Invalid chain '${input}'. Valid options are: ${CWCConst.CHAINS.join(', ')}`;
}
});
if (prompt.isCancel(chain)) {
throw new UserCancelled();
Expand Down Expand Up @@ -147,14 +153,17 @@ export async function getCopayerName() {
return copayerName as string;
};

export async function getAddressType({ chain, network, isMultiSig }: { chain: string; network?: Network; isMultiSig?: boolean }) {
export async function getAddressType(args: { chain: string; network?: Network; isMultiSig?: boolean; isTss?: boolean; }) {
const { chain, network, isMultiSig, isTss } = args;
let addressTypes = Constants.ADDRESS_TYPE[chain.toUpperCase()];
if (!addressTypes) {
return Constants.ADDRESS_TYPE.default;
}

if (isMultiSig) {
addressTypes = addressTypes.multiSig;
} else if (isTss) {
addressTypes = addressTypes.thresholdSig;
} else {
addressTypes = addressTypes.singleSig;
}
Expand Down
10 changes: 3 additions & 7 deletions packages/bitcore-cli/src/tss.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as prompt from '@clack/prompts';
import { TssSign, Utils as BWCUtils } from 'bitcore-wallet-client';
import { ethers, type Types as CWCTypes } from 'crypto-wallet-core';
import { TssSign } from 'bitcore-wallet-client';
import { Transactions, type Types as CWCTypes } from 'crypto-wallet-core';
import url from 'url';
import {
type TssKeyType,
Expand All @@ -25,12 +25,8 @@ export async function sign(args: {
}): Promise<CWCTypes.Message.ISignedMessage<string>> {
const { host, chain, walletData, messageHash, derivationPath, password, id, logMessageWaiting, logMessageCompleted } = args;

const isEvm = BWCUtils.isEvmChain(chain);

const transformISignature = (signature: TssSign.ISignature): string => {
if (isEvm) {
return ethers.Signature.from(signature).serialized;
}
return Transactions.transformSignatureObject({ chain, obj: signature });
};

const tssSign = new TssSign.TssSign({
Expand Down
61 changes: 55 additions & 6 deletions packages/bitcore-cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ export class Utils {
return Constants.COLOR[color.toLowerCase()].replace('%s', text);
}

static boldText(text: string) {
return '\x1b[1m' + text + '\x1b[0m';
}

static italicText(text: string) {
return '\x1b[3m' + text + '\x1b[0m';
}

static underlineText(text: string) {
return '\x1b[4m' + text + '\x1b[0m';
}

static strikeText(text: string) {
return '\x1b[9m' + text + '\x1b[0m';
}

static capitalize(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1);
}
Expand Down Expand Up @@ -95,8 +111,8 @@ export class Utils {
return amountSat;
};

static renderAmount(currency: string, satoshis: number | bigint, opts = {}): string {
return BWCUtils.formatAmount(satoshis, currency.toLowerCase(), { ...opts, fullPrecision: true }) + ' ' + currency.toUpperCase();
static renderAmount(currency: string, satoshis: number | bigint | string, opts?: ITokenObj): string {
return Utils.amountFromSats(currency, Number(satoshis), opts) + ' ' + currency.toUpperCase();
}

static renderStatus(status: string): string {
Expand Down Expand Up @@ -249,7 +265,7 @@ export class Utils {
case 'drops':
case 'lamports':
default:
`${feeRate} ${feeUnit}`;
return `${feeRate} ${feeUnit}`;
}
}

Expand All @@ -269,12 +285,12 @@ export class Utils {
case 'doge':
case 'ltc':
case 'xrp':
return sats / 1e8;
return (sats / 1e8).toLocaleString('fullwide', { useGrouping: false, minimumFractionDigits: 0, maximumFractionDigits: 8 });
case 'sol':
return sats / 1e9;
return (sats / 1e9).toLocaleString('fullwide', { useGrouping: false, minimumFractionDigits: 0, maximumFractionDigits: 9 });
default:
// Assume EVM chain
return sats / 1e18;
return (sats / 1e18).toLocaleString('fullwide', { useGrouping: false, minimumFractionDigits: 0, maximumFractionDigits: 18 });
}
}

Expand Down Expand Up @@ -371,4 +387,37 @@ export class Utils {
}
return fileName;
}

static getChainColor(chain: string) {
switch (chain.toLowerCase()) {
case 'btc':
return 'orange';
case 'bch':
return 'green';
case 'doge':
return 'beige';
case 'ltc':
return 'lightgray';
case 'eth':
return 'blue';
case 'matic':
return 'pink';
case 'xrp':
return 'darkgray';
case 'sol':
return 'purple';
}
}

static colorTextByChain(chain: string, text: string) {
const color = Utils.getChainColor(chain);
if (!color) {
return Utils.boldText(text);
}
return Utils.colorText(text, color);
}

static colorizeChain(chain: string) {
return Utils.colorTextByChain(chain, chain);
}
};
Loading