Skip to content
Draft
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
648 changes: 648 additions & 0 deletions packages/bitcore-cli/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/bitcore-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,17 @@
"usb": "2.15.0"
},
"devDependencies": {
"@bitpay-labs/bitcore-wallet-service": "^11.6.0",
"@types/chai": "5.2.2",
"@types/mocha": "^10.0.10",
"@types/node": "^22.14.1",
"chai": "^5.2.1",
"mocha": "^11.7.1",
"mongodb": "^3.5.9",
"nyc": "^17.1.0",
"sinon": "^21.0.0",
"source-map-support": "0.5.16",
"supertest": "^7.2.2",
"typescript": "^5.8.3"
},
"nyc": {
Expand Down
8 changes: 5 additions & 3 deletions packages/bitcore-cli/src/commands/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ export async function setToken(args: CommonArgs) {
const currencies = await Wallet.getCurrencies(wallet.network);
function findTokenObj(value) {
return currencies.find(c =>
c.contractAddress?.toLowerCase() === value.toLowerCase() ||
c.displayCode?.toLowerCase() === value.toLowerCase() ||
c.code?.toLowerCase() === value.toLowerCase()
c.chain?.toUpperCase() === wallet.chain.toUpperCase() && (
c.contractAddress?.toLowerCase() === value.toLowerCase() ||
c.displayCode?.toLowerCase() === value.toLowerCase() ||
c.code?.toLowerCase() === value.toLowerCase()
)
);
};

Expand Down
3 changes: 3 additions & 0 deletions packages/bitcore-cli/src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ export class Wallet implements IWallet {
testnet: process.env['BITCORE_CLI_CURRENCIES_URL'] || 'https://test.bitpay.com/currencies',
regtest: process.env['BITCORE_CLI_CURRENCIES_URL_REGTEST']
};
if (network === 'regtest' && !urls[network]) {
throw new Error('Set BITCORE_CLI_CURRENCIES_URL_REGTEST environment variable.');
}
let response: Response;
try {
response = await fetch(urls[network], { method: 'GET', headers: { 'Content-Type': 'application/json' } });
Expand Down
4 changes: 2 additions & 2 deletions packages/bitcore-cli/test/commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { execSync } from 'child_process';
import assert from 'assert';
import { getCommands } from '../src/cli-commands';
import { type IWallet } from '../types/wallet';
import { bitcoreLogo } from '../src/constants';
import { type ICliOptions } from 'types/cli';
import type { IWallet } from '../types/wallet';
import type { ICliOptions } from '../types/cli';

describe('Option: --command', function() {
const COMMANDS = getCommands({ wallet: {} as IWallet, opts: { command: 'any' } as ICliOptions });
Expand Down
83 changes: 83 additions & 0 deletions packages/bitcore-cli/test/create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { spawn } from 'child_process';
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { Transform } from 'stream';
import * as helpers from './helpers';

describe('Create', function() {
const KEYSTROKES = {
ENTER: '\r', // Enter/Return
ARROW_UP: '\x1b[A', // Arrow Up
ARROW_DOWN: '\x1b[B', // Arrow Down
ARROW_RIGHT: '\x1b[C', // Arrow Right
ARROW_LEFT: '\x1b[D', // Arrow Left
DELETE: '\x1b[3~', // Delete
BACKSPACE: '\x7f', // Backspace
CTRL_C: '\x03', // Ctrl+C
};
const cliDotJs = 'build/src/cli.js';
const tempWalletDir = path.join(__dirname, './wallets/temp');
const commonOpts = ['--verbose', '--host', 'http://localhost:3232', '--dir', tempWalletDir];

function cleanupWallets() {
if (fs.existsSync(tempWalletDir)) {
fs.rmdirSync(tempWalletDir, { recursive: true });
}
}

before(async function() {
cleanupWallets();
await helpers.startBws();
});

after(async function() {
await helpers.stopBws();
cleanupWallets();

});

describe('Single Sig', function() {
it('should create a BTC wallet', function(done) {
const stepInputs = [
[KEYSTROKES.ENTER], // Create Wallet
[KEYSTROKES.ENTER], // Chain: btc
['regtest', KEYSTROKES.ENTER], // Network: regtest
[KEYSTROKES.ENTER], // Multi-party? No
[KEYSTROKES.ENTER], // Address Type: default
['testpassword', KEYSTROKES.ENTER], // Password

];
let step = 0;
const io = new Transform({
encoding: 'utf-8',
transform(chunk, encoding, respond) {
chunk = chunk.toString();
// Uncomment to see CLI output during test
// process.stdout.write(chunk);

const isStep = chunk.endsWith('└\n');// chunk.includes('What would you like to do?') || chunk.includes('Network:');
if (isStep) {
for (const input of stepInputs[step]) {
this.push(input);
}
step++;
} else if (chunk.includes('Error:')) {
return respond(chunk);
}
respond();
}
});
const child = spawn('node', [cliDotJs, 'btc-temp', ...commonOpts]);
child.stderr.pipe(process.stderr);
child.stdout.pipe(io).pipe(child.stdin);
let err;
io.on('error', (e) => {
err = e;
});
io.on('close', () => {
done(err);
});
});
});
});
12 changes: 12 additions & 0 deletions packages/bitcore-cli/test/data/test-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const host = process.env.DB_HOST || 'localhost';
const port = process.env.DB_PORT || '27017';
const dbname = 'cli_test';

const config = {
mongoDb: {
uri: `mongodb://${host}:${port}/${dbname}`,
dbname,
},
};

export default config;
185 changes: 185 additions & 0 deletions packages/bitcore-cli/test/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import sinon from 'sinon';
import assert from 'assert';
import * as CWC from '@bitpay-labs/crypto-wallet-core';
import BWS from '@bitpay-labs/bitcore-wallet-service';
import { API } from '@bitpay-labs/bitcore-wallet-client';
import { Constants } from '@bitpay-labs/bitcore-wallet-client/src/lib/common/constants';
import { MongoClient } from 'mongodb';
import supertest from 'supertest';
import config from '../test/data/test-config';

const Bitcore = CWC.BitcoreLib;
const Bitcore_ = {
btc: CWC.BitcoreLib,
bch: CWC.BitcoreLibCash
};
const { ExpressApp, Storage } = BWS;

let client: MongoClient;
let expressApp: InstanceType<typeof ExpressApp>;

export async function newDb() {
client = await MongoClient.connect(config.mongoDb.uri);
const db = client.db(config.mongoDb.dbname);
await db.dropDatabase();
return { client, db };
}

export async function startBws() {
const { db } = await newDb();
const storage = new Storage({ db });
Storage.createIndexes(db);
expressApp = new ExpressApp();
return new Promise<void>(resolve => {
expressApp.start(
{
ignoreRateLimiter: true,
storage: storage,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
doNotCheckV8: true
},
() => {
sinon.stub(API.prototype, 'constructor').callsFake(function(opts) {
opts.request = supertest(expressApp.app);
return (API.prototype.constructor as any).wrappedMethod.call(API.prototype, opts);
});
resolve();
}
);
});
}

export async function stopBws() {
return new Promise<void>(resolve => {
expressApp.app.removeAllListeners();
client.close(false, resolve);
});
};

export const blockchainExplorerMock = {
register: sinon.stub().callsArgWith(1, null, null),
getCheckData: sinon.stub().callsArgWith(1, null, { sum: 100 }),
addAddresses: sinon.stub().callsArgWith(2, null, null),
utxos: [],
lastBroadcasted: null,
txHistory: [],
feeLevels: [],
getUtxos: (wallet, height, cb) => {
return cb(null, JSON.parse(JSON.stringify(blockchainExplorerMock.utxos)));
},
getAddressUtxos: (address, height, cb) => {
const selected = blockchainExplorerMock.utxos.filter(utxo => {
return address.includes(utxo.address);
});

return cb(null, JSON.parse(JSON.stringify(selected)));
},
setUtxo: (address, amount, m, confirmations?) => {
const B = Bitcore_[address.coin];
let scriptPubKey;
switch (address.type) {
case Constants.SCRIPT_TYPES.P2SH:
scriptPubKey = address.publicKeys ? B.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut() : '';
break;
case Constants.SCRIPT_TYPES.P2WPKH:
case Constants.SCRIPT_TYPES.P2PKH:
scriptPubKey = B.Script.buildPublicKeyHashOut(address.address);
break;
case Constants.SCRIPT_TYPES.P2WSH:
scriptPubKey = B.Script.buildWitnessV0Out(address.address);
break;
}
assert(!!scriptPubKey, 'scriptPubKey not defined');
blockchainExplorerMock.utxos.push({
txid: new Bitcore.crypto.Hash.sha256(Buffer.alloc(Math.random() * 100000)).toString('hex'),
outputIndex: 0,
amount: amount,
satoshis: amount * 1e8,
address: address.address,
scriptPubKey: scriptPubKey.toBuffer().toString('hex'),
confirmations: confirmations == null ? Math.floor(Math.random() * 100 + 1) : +confirmations
});
},
supportsGrouping: () => {
return false;
},
getBlockchainHeight: cb => {
return cb(null, 1000);
},
broadcast: (raw, cb) => {
blockchainExplorerMock.lastBroadcasted = raw;

let hash;
try {
const tx = new Bitcore.Transaction(raw);
if (!tx.outputs.length) {
throw 'no bitcoin';
}
hash = tx.id;
// btc/bch
return cb(null, hash);
} catch (e) {
// try eth
hash = CWC.Transactions.getHash({
tx: raw[0],
chain: 'ETH'
});
return cb(null, hash);
}
},
setHistory: txs => {
blockchainExplorerMock.txHistory = txs;
},
getTransaction: (txid, cb) => {
return cb();
},
getTransactions: (wallet, startBlock, cb) => {
let list = [].concat(blockchainExplorerMock.txHistory);
// -1 = mempool, always included in server' s v8.js
list = list.filter(x => {
return x.height >= startBlock || x.height == -1;
});
return cb(null, list);
},
getAddressActivity: (address, cb) => {
const activeAddresses = blockchainExplorerMock.utxos.map(u => u.address);
return cb(null, activeAddresses.includes(address));
},
setFeeLevels: levels => {
blockchainExplorerMock.feeLevels = levels;
},
estimateFee: (nbBlocks, cb) => {
const levels = {};
for (const nb of nbBlocks) {
const feePerKb = blockchainExplorerMock.feeLevels[nb];
levels[nb] = typeof feePerKb === 'number' ? feePerKb / 1e8 : -1;
}

return cb(null, levels);
},
estimateFeeV2: (opts, cb) => {
return cb(null, 20000);
},
estimatePriorityFee: (opts, cb) => {
return cb(null, 5000);
},
estimateGas: (nbBlocks, cb) => {
return cb(null, '20000000000');
},
getBalance: (nbBlocks, cb) => {
return cb(null, {
unconfirmed: 0,
confirmed: 20000000000 * 5,
balance: 20000000000 * 5
});
},
getTransactionCount: (addr, cb) => {
return cb(null, 0);
},
reset: () => {
blockchainExplorerMock.utxos = [];
blockchainExplorerMock.txHistory = [];
blockchainExplorerMock.feeLevels = [];
}
};
1 change: 1 addition & 0 deletions packages/bitcore-cli/test/wallets/btc-singlesig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"key":{"xPrivKey":"xprv9s21ZrQH143K3UxJbdjoUtVLgoUHNDafRx9PX7DvyjczjtgznRkqkMmqiEJ2XeHnuJxqNCR93xwg3a169NMc9FiXoYdyrk4jZruDwCoxWeV","xPrivKeyEDDSA":"xprv9s21ZrQH143K4LDzPgFhCGd3qbeMAdGoPmVVX2Q9vzeDw12sEZMHtyuYv5j8hvq66EgY1ES2e6SwUPNEa9ZSQ91cEpEW2hJkNhi1ZAFR8zr","mnemonic":"grab soap kitchen suggest salt quiz slogan candy cash note general dove","version":1,"mnemonicHasPassphrase":false,"fingerPrint":"e4794b6b","fingerPrintEDDSA":"ec84e87d","compliantDerivation":true,"id":"70123710-2d71-42ce-b09c-e260dadf4631"},"credentials":{"coin":"btc","chain":"btc","network":"regtest","xPubKey":"tpubDCKPndk1XFyHP68Znm9u2aAh337uzYuAskuRC8aN73iDeWjXtetsZg55exJkaAxq64wZkU8Ea5gsyazYM7ourdK4nQcZVF2kHYTwaNUsr7F","requestPrivKey":"0b491780ba50f1cf42c2b0ad816a247d034293e0ae53eb47b0d27e59996dc5dd","requestPubKey":"03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79","copayerId":"90348b306bb58881013c63fe1238eddabf327836b2be808cdc9ebf97a426d9a5","publicKeyRing":[{"xPubKey":"tpubDCKPndk1XFyHP68Znm9u2aAh337uzYuAskuRC8aN73iDeWjXtetsZg55exJkaAxq64wZkU8Ea5gsyazYM7ourdK4nQcZVF2kHYTwaNUsr7F","requestPubKey":"03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79"}],"m":1,"n":1,"personalEncryptingKey":"/idxpn6qYww42TFH1e+ATw==","copayerName":"kjoseph","account":0,"addressType":"witnesspubkeyhash","version":2,"rootPath":"m/44'/0'/0'","keyId":"70123710-2d71-42ce-b09c-e260dadf4631"}}