Skip to content
Open
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