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
3 changes: 1 addition & 2 deletions packages/bitcore-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../packa

program
.addHelpText('beforeAll', bitcoreLogo)
.usage('<walletName> [options]')
.usage('<walletName>|list [options]')
.description('A command line tool for Bitcore wallets')
.argument('<walletName>', 'Name of the wallet you want to create, join, or interact with. Use "list" to see all wallets in the specified directory.')
.optionsGroup('Global Options')
Expand All @@ -31,7 +31,6 @@ program
.option('--no-status', 'Do not display the wallet status on startup. Defaults to true when running with --command')
.option('-s, --pageSize <number>', 'Number of items per page of a list output', (value) => parseInt(value, 10), 10)
.option('-v, --verbose', 'Show more data and logs')
.option('--list', 'See all wallets in the specified directory')
.option('--register', 'Register the wallet with the Bitcore Wallet Service if it does not exist')
.option('--walletId <walletId>', 'Support Staff Only: Wallet ID to provide support for')
.option('-h, --help', 'Display help message. Use with --command to get help for a specific command')
Expand Down
61 changes: 40 additions & 21 deletions packages/bitcore-cli/src/commands/create/createThresholdSig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,47 @@ export async function createThresholdSigWallet(
extra: tssPassword
});

const goBack = await prompt.select({
message: `Join code for party ${i}:${os.EOL}${joinCode}`,
initialValue: false,
options: [
{
label: 'Continue →',
value: false
},
{
label: '↩ Go Back',
value: true,
hint: `Re-enter party ${i}'s public key`
}
]
});
if (prompt.isCancel(goBack)) {
throw new UserCancelled();
}
let joinCodeAction: 'copy' | 'continue' | 'goBack' | symbol;
do {
joinCodeAction = await prompt.select({
message: joinCodeAction === 'copy' ? 'Copied!' : `Join code for party ${i}:${os.EOL}${joinCode}`,
initialValue: joinCodeAction === 'copy' ? 'continue' : 'copy',
options: [
{
label: 'Continue →',
value: 'continue'
},
{
label: 'Copy to clipboard ⎘',
value: 'copy'
},
{
label: '↩ Go Back',
value: 'goBack',
hint: `Re-enter party ${i}'s public key`
}
]
});
if (prompt.isCancel(joinCodeAction)) {
throw new UserCancelled();
}

if (goBack) {
i--; // Retry this party
}
switch (joinCodeAction) {
case 'goBack':
i--; // Retry this party
break;
case 'copy':
try {
Utils.copyToClipboard(joinCode);
} catch (error) {
prompt.log.error(`Error copying to clipboard: ${error instanceof Error ? error.message : String(error)}`);
joinCodeAction = null; // Reset to re-prompt the user
}
break;
case 'continue':
break;
}
} while (joinCodeAction !== 'continue');
}

const spinner = prompt.spinner({ indicator: 'timer' });
Expand Down
29 changes: 22 additions & 7 deletions packages/bitcore-cli/src/commands/join/joinThresholdSig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,28 @@ export async function joinThresholdSigWallet(
});

const authPubKey = tss.getAuthPublicKey();
const done = await prompt.select({
message: `Give the following public key to the session leader:${os.EOL}${Utils.colorText(authPubKey, 'blue')}`,
options: [{ label: 'Done', value: true, hint: 'Hit Enter/Return to continue' }]
});
if (prompt.isCancel(done)) {
throw new UserCancelled();
}
let pubKeyAction: 'copy' | 'done' | symbol;
do {
pubKeyAction = await prompt.select({
message: pubKeyAction === 'copy' ? 'Copied!' : `Give the following public key to the session leader:${os.EOL}${Utils.colorText(authPubKey, 'blue')}`,
initialValue: pubKeyAction === 'copy' ? 'done' : 'copy',
options: [
{ label: 'Done', value: 'done', hint: 'Hit Enter/Return to continue' },
{ label: 'Copy to clipboard ⎘', value: 'copy' }
]
});
if (prompt.isCancel(pubKeyAction)) {
throw new UserCancelled();
}
if (pubKeyAction === 'copy') {
try {
Utils.copyToClipboard(authPubKey);
} catch (error) {
prompt.log.error(`Error copying to clipboard: ${error instanceof Error ? error.message : String(error)}`);
pubKeyAction = null; // Reset to re-prompt the user
}
}
} while (pubKeyAction !== 'done');

const joinCode = await prompt.text({
message: 'Enter the join code from the session leader:',
Expand Down
10 changes: 5 additions & 5 deletions packages/bitcore-cli/src/commands/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export async function createTransaction(
return; // valid value, optional
}
const val = parseInt(value);
if (isNaN(val) || val < 0) {
if (isNaN(val) || val < 0 || !(/^\d+$/.test(value))) {
return 'Please enter a valid destination tag';
}
return; // valid value
Expand Down Expand Up @@ -256,7 +256,7 @@ export async function createTransaction(
throw new UserCancelled();
}
if (BWCUtils.isUtxoChain(chain)) {
customFeeRate = (Number(customFeeRate) * 1000).toString(); // convert to sats/KB
customFeeRate = Math.round(Number(customFeeRate) * 1000).toString(); // convert to sats/KB
}
}

Expand All @@ -267,8 +267,8 @@ export async function createTransaction(
}],
message: note,
feeLevel: feeLevel === 'custom' ? undefined : feeLevel,
feePerKb: feeLevel === 'custom' ? parseFloat(customFeeRate) : undefined,
fee: opts.fee ? parseFloat(opts.fee) : undefined,
feePerKb: feeLevel === 'custom' ? BigInt(Math.ceil(Number(customFeeRate))) : undefined,
fee: opts.fee ? BigInt(Math.ceil(parseFloat(opts.fee))) : undefined,
sendMax,
tokenAddress: tokenObj?.contractAddress,
flags: opts.flags,
Expand All @@ -294,7 +294,7 @@ export async function createTransaction(
: Utils.renderAmount(currency, BigInt(txp.amount) + BigInt(txp.fee))
}`);
if (txp.nonce != null) {
lines.push(`Nonce: ${txp.nonce}`);
lines.push(`Nonce: ${BigInt(txp.nonce)}`);
}
if (note) {
lines.push(`Note: ${txp.message}`);
Expand Down
4 changes: 2 additions & 2 deletions packages/bitcore-cli/src/commands/txproposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ export async function getTxProposals(

const extraChoices = []
.concat(
txp.status !== 'broadcasted' && !txp.actions.find(a => a.copayerId === myCopayerId) && txp.status !== 'deleted' ? [
txp.status !== 'broadcasted' && !txp.actions?.find(a => a.copayerId === myCopayerId) && txp.status !== 'deleted' ? [
{ label: 'Accept', value: ViewAction.ACCEPT, hint: 'Accept and sign this proposal' },
{ label: 'Reject', value: ViewAction.REJECT, hint: 'Reject this proposal' },
] : []
).concat(
txp.status !== 'broadcasted' && txp.actions.filter(a => a.type === 'accept').length >= txp.requiredSignatures && txp.status !== 'deleted' ? [
txp.status !== 'broadcasted' && txp.actions?.filter(a => a.type === 'accept').length >= txp.requiredSignatures && txp.status !== 'deleted' ? [
{ label: 'Broadcast', value: ViewAction.BROADCAST, hint: 'Broadcast this proposal' }
] : []
).concat(
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcore-cli/src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export async function promptKeyshareBackup(): Promise<boolean> {
'This keyshare backup file contains both your 12-word mnemonic AND your keyshare data, encrypted with a password you will set in the following prompts.' + os.EOL +
'Make sure to:' + os.EOL +
` - Store the file in a ${Utils.underlineText('safe place')}, like a USB drive in a safe, and do not share it with anyone.` + os.EOL +
` - ${Utils.boldText('DO NOT FORGET')} the encryption password! The file is useless without it, and there is no way to reset the password.` + os.EOL +
` - ${Utils.boldText('DO NOT FORGET', true)} the encryption password! The file is useless without it, and there is no way to reset the password.` + os.EOL +
'Both the file + encryption password are as valuable as a non-TSS wallet\'s 12-24 word phrase, so treat them with the same level of security.'
);
const a = await prompt.select({
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcore-cli/src/tss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function sign(args: {

tssSign.subscribe();
tssSign.on('roundsubmitted', (round) => spinner.message(`Round ${round} submitted`));
tssSign.on('error', prompt.log.error);
tssSign.on('error', e => prompt.log.error('Unexpected error during TSS signing: ' + e));
tssSign.on('complete', async () => {
try {
spinner.stop(logMessageCompleted || 'TSS signature generated');
Expand Down
74 changes: 72 additions & 2 deletions packages/bitcore-cli/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type StdioOptions, spawnSync } from 'child_process';
import os from 'os';
import path from 'path';
import * as prompt from '@clack/prompts';
Expand Down Expand Up @@ -66,8 +67,12 @@ export class Utils {
return Constants.COLOR[color.toLowerCase()].replace('%s', text);
}

static boldText(text: string) {
return '\x1b[1m' + text + '\x1b[22m';
static boldText(text: string, isDim?: boolean) {
return '\x1b[1m' + text + '\x1b[22m' + (isDim ? '\x1b[2m' : ''); // 22 is the ANSI code to turn off bold AND dim. So, need to re-apply dim if applicable
}

static dimText(text: string, isBold?: boolean) {
return '\x1b[2m' + text + '\x1b[22m' + (isBold ? '\x1b[1m' : ''); // 22 is the ANSI code to turn off bold AND dim. So, need to re-apply bold if applicable
}

static italicText(text: string) {
Expand Down Expand Up @@ -436,4 +441,69 @@ export class Utils {
static colorizeChain(chain: string) {
return Utils.colorTextByChain(chain, chain);
}

static copyToClipboard(text: string): void {
const platform = os.platform();
let attempts: Array<{ cmd: string; args: string[] }>;

if (platform === 'darwin') {
attempts = [{ cmd: 'pbcopy', args: [] }];
} else if (platform === 'linux') {
// Prefer wl-copy first (Wayland), then fall back to X11 tools.
attempts = [
{ cmd: 'wl-copy', args: [] },
{ cmd: 'xclip', args: ['-selection', 'clipboard'] },
{ cmd: 'xsel', args: ['--clipboard', '--input'] }
];
} else if (platform === 'win32') {
attempts = [{ cmd: 'clip', args: [] }];
} else {
throw new Error(`Unsupported platform: ${platform}`);
}

const missing: string[] = [];
const failures: string[] = [];

for (const attempt of attempts) {
// wl-copy can fork and keep inherited pipes open; piping stderr/stdout can
// cause spawnSync to block even after successful copy.
const stdio: StdioOptions = attempt.cmd === 'wl-copy'
? ['pipe', 'ignore', 'ignore']
: ['pipe', 'ignore', 'pipe'];

const result = spawnSync(attempt.cmd, attempt.args, {
input: text,
encoding: 'utf8',
stdio
});

if (result.error) {
const err = result.error as NodeJS.ErrnoException;
if (err.code === 'ENOENT') {
missing.push(attempt.cmd);
continue;
}
failures.push(`${attempt.cmd}: ${err.message}`);
continue;
}

if (result.status === 0) {
return;
}

const stderr = (result.stderr ?? '').trim();
failures.push(`${attempt.cmd}: ${stderr || `exited with code ${result.status}`}`);
}

if (missing.length === attempts.length) {
throw new Error(`No clipboard utility found. Tried: ${attempts.map(a => a.cmd).join(', ')}`);
}

const detailParts = [
failures.length ? `Failures: ${failures.join(' | ')}` : '',
missing.length ? `Not installed: ${missing.join(', ')}` : ''
].filter(Boolean);

throw new Error(`Failed to copy to clipboard. ${detailParts.join(' ; ')}`);
}
};
Loading