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
63 changes: 41 additions & 22 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 All @@ -112,7 +131,7 @@ export async function createThresholdSigWallet(
createWalletOpts: Utils.getSegwitInfo(addressType)
});
tss.on('roundsubmitted', (round) => spinner.message(`Round ${round} submitted`));
tss.on('error', prompt.log.error);
tss.on('error', e => prompt.log.error('Unexpected error during TSS wallet creation: ' + (e.stack || e)));
tss.on('wallet', async (_wallet) => {
// TODO: what to do with the wallet?
// console.log('Created wallet at BWS:', wallet);
Expand Down
31 changes: 23 additions & 8 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth cleaning up the TS errors for using pubKeyAction before it's defined. Give it an initial value and add it to the type declaration?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing any such typescript errors. Maybe your editor's TS version is 6.x instead of the workspace's 5.x?

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 Expand Up @@ -84,7 +99,7 @@ export async function joinThresholdSigWallet(
});
tss.subscribe({ copayerName });
tss.on('roundsubmitted', (round) => spinner.message(`Round ${round} submitted`));
tss.on('error', prompt.log.error);
tss.on('error', e => prompt.log.error('Unexpected error during TSS wallet creation: ' + (e.stack || e)));
tss.on('wallet', async (_wallet) => {
// TOOD: what to do with this?
// console.log('Joined wallet at BWS:', wallet);
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
41 changes: 26 additions & 15 deletions packages/bitcore-cli/src/commands/txproposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export async function getTxProposals(
prompt.log.success(`Exported to ${outputFile}`);

} else if (printRaw) {
prompt.log.info(`ID: ${txp.id}` + os.EOL + JSON.stringify(txp, null, 2));
console.log(JSON.stringify(txp, null, 2));

} else {
const lines = [];
Expand Down Expand Up @@ -223,24 +223,35 @@ export async function getTxProposals(
return {}; // Don't wait for user input in CLI mode
}

const extraChoices = []
.concat(
txp.status !== 'broadcasted' && !txp.actions.find(a => a.copayerId === myCopayerId) && txp.status !== 'deleted' ? [
const extraChoices: Array<{ label: string; value: string; hint?: string }> = [];

if (txp.status !== 'broadcasted' && txp.status !== 'deleted') {
if (!txp.actions?.find(a => a.copayerId === myCopayerId)) {
extraChoices.push(
{ 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' ? [
);
}

if (txp.actions?.filter(a => a.type === 'accept').length >= txp.requiredSignatures) {
extraChoices.push(
{ label: 'Broadcast', value: ViewAction.BROADCAST, hint: 'Broadcast this proposal' }
] : []
).concat(
txp.status !== 'broadcasted' && txp.status !== 'rejected' && txp.status !== 'deleted' ? [
);
}

if (txp.status !== 'rejected') {
extraChoices.push(
{ label: 'Delete', value: ViewAction.DELETE, hint: 'Delete this proposal' }
] : []
).concat([
printRaw ? { label: 'Print Pretty', value: ViewAction.TOGGLE_RAW, hint: 'Print formatted proposal' } : { label: 'Print Raw Object', value: ViewAction.TOGGLE_RAW, hint: 'Print raw proposal object' },
{ label: 'Export', value: ViewAction.EXPORT, hint: 'Save to a file' },
]);
);
}
}

extraChoices.push(
printRaw ?
{ label: 'Print Pretty', value: ViewAction.TOGGLE_RAW, hint: 'Print formatted proposal' } :
{ label: 'Print Raw Object', value: ViewAction.TOGGLE_RAW, hint: 'Print raw proposal object' },
{ label: 'Export', value: ViewAction.EXPORT, hint: 'Save to a file' }
);

return { result: txps, extraChoices, hasNextPage, hasPrevPage };
}, {
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.stack || e)));
tssSign.on('complete', async () => {
Comment thread
kajoseph marked this conversation as resolved.
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[] }>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declaring as constant and pushing would mitigate potential downstream reassigns that you don't want.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine leaving it as is. Pushing implies it's additive. I don't want to be adding. I want it to be defining based on the platform which is more intuitive when you see an assignment than a push. This function is not particularly complicated. I don't think it's a big risk that someone would overwrite it.


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