Skip to content
2 changes: 1 addition & 1 deletion packages/bitcore-cli/src/commands/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export async function createTransaction(
lines.push(`Fee: ${Utils.renderAmount(chain, txp.fee)} (${Utils.displayFeeRate(chain, txp.feePerKb)})`);
lines.push(`Total: ${tokenObj
? Utils.renderAmount(currency, txp.amount, tokenObj) + ` + ${Utils.renderAmount(chain, txp.fee)}`
: Utils.renderAmount(currency, txp.amount + txp.fee)
: Utils.renderAmount(currency, BigInt(txp.amount) + BigInt(txp.fee))
}`);
if (txp.nonce != null) {
lines.push(`Nonce: ${txp.nonce}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcore-cli/src/commands/txproposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export async function getTxProposals(
// 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)
: Utils.renderAmount(currency, BigInt(txp.amount) + BigInt(txp.fee))
}`);
txp.gasPrice && lines.push(`Gas Price: ${Utils.displayFeeRate(chain, txp.gasPrice)}`);
txp.gasLimit && lines.push(`Gas Limit: ${txp.gasLimit}`);
Expand Down
10 changes: 5 additions & 5 deletions packages/bitcore-cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,22 +285,22 @@ export class Utils {
}
}

static displayFeeRate(chain: string, feeRate: number) {
static displayFeeRate(chain: string, feeRate: number | string | bigint) {
chain = chain.toLowerCase();
const feeUnit = Utils.getFeeUnit(chain);
switch (feeUnit) {
case 'sat/kB':
return `${feeRate / 1000} sat/B`;
return `${Number(feeRate) / 1000} sat/B`;
case 'gwei':
return `${feeRate / 1e9} Gwei`;
return `${Number(feeRate) / 1e9} Gwei`;
case 'drops':
case 'lamports':
default:
return `${feeRate} ${feeUnit}`;
return `${Number(feeRate)} ${feeUnit}`;
}
}

static convertFeeRate(chain: string, feeRate: number) {
static convertFeeRate(chain: string, feeRate: number | string): number {
const feeRateStr = Utils.displayFeeRate(chain, feeRate);
return parseFloat(feeRateStr.split(' ')[0]);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/bitcore-cli/test/proposals.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn } from 'child_process';
import assert from 'assert';
import sinon from 'sinon';
import { Transform } from 'stream';
import * as helpers from './helpers';
import * as walletData from './data/walletsData';
Expand All @@ -15,10 +16,12 @@ describe('Proposals', function() {
before(async function() {
await helpers.startBws();
await helpers.loadWalletData(walletData.btcSingleSigWallet);
sinon.stub(process, 'exit').throws(new Error('process.exit was called')); // prevent accidental exits during test
});

after(async function() {
await helpers.stopBws();
sinon.restore();
});

it('should show no pending proposals', function(done) {
Expand Down
2 changes: 0 additions & 2 deletions packages/bitcore-wallet-client/.coveralls.yml

This file was deleted.

10 changes: 0 additions & 10 deletions packages/bitcore-wallet-client/Gruntfile.js

This file was deleted.

66 changes: 49 additions & 17 deletions packages/bitcore-wallet-client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,9 +1397,10 @@ export class API extends EventEmitter {
opts = opts || {};

const qs = [];
qs.push('includeExtendedInfo=' + (opts.includeExtendedInfo ? '1' : '0'));
qs.push('twoStep=' + (opts.twoStep ? '1' : '0'));
qs.push(`includeExtendedInfo=${opts.includeExtendedInfo ? '1' : '0'}`);
qs.push(`twoStep=${opts.twoStep ? '1' : '0'}`);
qs.push('serverMessageArray=1');
qs.push('numberFormat=hex'); // Only applies to `pendingTxps` in response. TODO apply this to balances as well.

if (opts.tokenAddress) {
qs.push('tokenAddress=' + opts.tokenAddress);
Expand Down Expand Up @@ -1701,6 +1702,11 @@ export class API extends EventEmitter {
opts: {
/** The transaction proposal object returned by the API#createTxProposal method */
txp: Txp;
/**
* Number format for the tx-building numbers (e.g. amounts, nonce, etc.). Default: 'hex'
* Note: The given `txp` will be converted server-side and returned in the specified format.
*/
numberFormat?: 'hex' | 'number' | 'string';
},
/** @deprecated */
cb?: (err?: Error, txp?: any) => void
Expand All @@ -1719,8 +1725,9 @@ export class API extends EventEmitter {
const args = {
proposalSignature: Utils.signMessage(hash, this.credentials.requestPrivKey)
};
const qs = `numberFormat=${opts.numberFormat || 'hex'}`;

const url = '/v2/txproposals/' + opts.txp.id + '/publish/';
const url = `/v2/txproposals/${opts.txp.id}/publish?${qs}`;
const { body: txp } = await this.request.post<object, PublishedTxp>(url, args);
this._processTxps(txp);
if (cb) { cb(null, txp); }
Expand Down Expand Up @@ -1908,6 +1915,8 @@ export class API extends EventEmitter {
forAirGapped?: boolean;
/** Do not encrypt the public key ring */
doNotEncryptPkr?: boolean;
/** Number format for the tx-building numbers (e.g. amounts, fee, nonce, etc.). Default: 'hex' */
numberFormat?: 'hex' | 'number' | 'string';
},
/** @deprecated */
cb?: (err?: Error, txps?: any[]) => void
Expand All @@ -1921,8 +1930,9 @@ export class API extends EventEmitter {

opts = opts || {};
const { doNotVerify, forAirGapped, doNotEncryptPkr } = opts;
const qs = `numberFormat=${opts.numberFormat || 'hex'}`;

const { body: txps } = await this.request.get('/v2/txproposals/');
const { body: txps } = await this.request.get(`/v2/txproposals?${qs}`);
this._processTxps(txps);

if (!doNotVerify) {
Expand Down Expand Up @@ -2051,8 +2061,18 @@ export class API extends EventEmitter {
const isLegit = Verifier.checkTxProposal(this.credentials, txp, { paypro });
if (!isLegit) throw new Errors.SERVER_COMPROMISED();

// Determine number format for the API request based on the given txp's values.
// This ensures the server maintains number precision when verifying signatures.
const amt = txp.amount || txp.outputs?.[0]?.amount;
const numberFormat = typeof amt === 'number'
? 'number'
: amt.startsWith('0x')
? 'hex'
: 'string';

const qs = `numberFormat=${numberFormat}`;
baseUrl = baseUrl || '/v2/txproposals/';
const url = `${baseUrl}${txp.id}/signatures/`;
const url = `${baseUrl}${txp.id}/signatures?${qs}`;
const args: any = { signatures, nonce: txp.nonce };
const { body: signedTxp } = await this.request.post<object, Txp>(url, args);
this._processTxps(signedTxp);
Expand All @@ -2069,11 +2089,23 @@ export class API extends EventEmitter {
* Assigns JIT values (nonce, and in the future: fee, gas) to a deferred txp.
* Call this just before signing a deferred-nonce txp.
*/
async prepareTx(opts: { txp: Txp }): Promise<Txp> {
$.checkState(this.credentials && this.credentials.isComplete(),
'Failed state: this.credentials at <prepareTx()>');

const url = '/v1/txproposals/' + opts.txp.id + '/prepare/';
async prepareTx(opts: {
txp: Txp;
}): Promise<Txp> {
$.checkState(this.credentials?.isComplete(), 'Failed state: this.credentials at <prepareTx()>');

// Determine number format for the API request based on the type of txp.amount.
// This ensures the server maintains number precision when verifying signatures.
const amt = opts.txp.amount || opts.txp.outputs?.[0]?.amount;
const numberFormat = typeof amt === 'number'
? 'number'
: amt.startsWith('0x')
? 'hex'
: 'string';

const qs = `numberFormat=${numberFormat}`;

const url = `/v1/txproposals/${opts.txp.id}/prepare?${qs}`;
const { body: txp } = await this.request.post<object, Txp>(url, {});
this._processTxps(txp);
return txp;
Expand Down Expand Up @@ -4189,7 +4221,7 @@ export interface Txp {
comment?: string;
}>; // TODO
addressType: string;
amount: number;
amount: number | string;
chain: string;
coin: string;
changeAddress?: {
Expand All @@ -4211,21 +4243,21 @@ export interface Txp {
creatorId: string;
creatorName?: string; // might be an encrypted object
excludeUnconfirmedUtxos: boolean;
fee: number;
fee: number | string;
feeLevel: string;
feePerKb: number;
feePerKb: number | string;
from?: string;
hasUnconfirmedInputs?: boolean;
id: string;
inputPaths: Array<string>;
inputs?: Array<{
address: string;
amount: number;
amount: number | string;
confirmations: number;
locked: boolean;
path: string;
publicKeys: Array<string>;
satoshis: number;
satoshis: number | string;
scriptPubKey: string;
spent: boolean;
txid: string;
Expand All @@ -4235,12 +4267,12 @@ export interface Txp {
message?: string; // might be an encrypted object
encryptedMessage?: string; // is set equal to `message` before decryption in processTxps()
network: string;
nonce?: number;
nonce?: number | string;
deferNonce?: boolean;
note?: Note;
outputOrder: Array<number>;
outputs?: Array<{
amount: number;
amount: number | string;
toAddress: string;
message?: string; // might be an encrypted object
encryptedMessage?: string; // is set equal to `message` before decryption in processTxps()
Expand Down
1 change: 1 addition & 0 deletions packages/bitcore-wallet-client/src/lib/bulkclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export class BulkClient extends Request<Array<Credentials>> {
qs.push('twoStep=' + (opts.twoStep ? '1' : '0'));
qs.push('serverMessageArray=1');
qs.push('silentFailure=' + (opts.silentFailure ? '1' : '0'));
qs.push('numberFormat=hex'); // Only applies to `pendingTxps` in response. TODO apply this to balances as well.

const wallets = opts.wallets;
if (wallets) {
Expand Down
18 changes: 9 additions & 9 deletions packages/bitcore-wallet-client/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('client API', function() {
let clients: Client[], app, sandbox, storage, keys, i;
let dbConnection;
let db;
this.timeout(8000);
this.timeout(Math.max(this['_timeout'], 8000));

before(function(done) {
i = 0;
Expand Down Expand Up @@ -4807,7 +4807,7 @@ describe('client API', function() {
should.not.exist(err);
const tx = txps[0];
// From the hardcoded paypro request
tx.outputs[0].amount.should.equal(DATA.instructions[0].outputs[0].amount);
parseInt(tx.outputs[0].amount).should.equal(DATA.instructions[0].outputs[0].amount);
tx.outputs[0].toAddress.should.equal(DATA.instructions[0].outputs[0].address);
tx.message.should.equal(DATA.memo);
tx.payProUrl.should.equal('https://bitpay.com/i/LanynqCPoL2JQb8z8s5Z3X');
Expand Down Expand Up @@ -4837,13 +4837,13 @@ describe('client API', function() {
should.not.exist(err);
const tx = txps[0];
// From the hardcoded paypro request
tx.outputs[0].amount.should.equal(DATA.instructions[0].outputs[0].amount);
parseInt(tx.outputs[0].amount).should.equal(DATA.instructions[0].outputs[0].amount);
tx.outputs[0].toAddress.should.equal(DATA.instructions[0].outputs[0].address);
tx.message.should.equal(DATA.memo);
tx.payProUrl.should.equal('https://bitpay.com/i/LanynqCPoL2JQb8z8s5Z3X');
done();
} catch (e) {
console.error(e);
done(e);
}
});
});
Expand Down Expand Up @@ -4987,7 +4987,7 @@ describe('client API', function() {
should.not.exist(err);
const tx = txps[0];

tx.outputs[0].amount.should.equal(DATA.instructions[0].outputs[0].amount);
parseInt(tx.outputs[0].amount).should.equal(DATA.instructions[0].outputs[0].amount);
tx.outputs[0].toAddress.should.equal(DATA.instructions[0].outputs[0].address);
tx.message.should.equal(DATA.memo);
tx.payProUrl.should.equal('https://bitpay.com/i/LanynqCPoL2JQb8z8s5Z3X');
Expand Down Expand Up @@ -5041,7 +5041,7 @@ describe('client API', function() {
const signatures = await keys[0].sign(clients[0].getRootPath(), txps[0]);
clients[0].pushSignatures(txps[0], signatures, async (err, xx) => {
should.not.exist(err);
xx.feePerKb /= 2;
xx.feePerKb = Number(xx.feePerKb) / 2;
const signatures2 = await keys[1].sign(clients[1].getRootPath(), xx);
clients[1].pushSignatures(xx, signatures2, (err, yy) => {
should.not.exist(err);
Expand Down Expand Up @@ -5241,7 +5241,7 @@ describe('client API', function() {
should.not.exist(err);
const tx = txps[0];
// From the hardcoded paypro request
tx.amount.should.equal(DATA.instructions[0].outputs[0].amount);
parseInt(tx.amount).should.equal(DATA.instructions[0].outputs[0].amount);
tx.outputs[0].toAddress.should.equal(DATA.instructions[0].outputs[0].address);
tx.message.should.equal(DATA.memo);
tx.payProUrl.should.equal('http://example.com');
Expand Down Expand Up @@ -5451,8 +5451,8 @@ describe('client API', function() {
txp.outputs[0].message.should.equal('output 0');
txp.message.should.equal('hello');
txp.txType.should.equal(2);
txp.maxGasFee.should.equal(20000);
txp.priorityGasFee.should.equal(5000);
txp.maxGasFee.should.equal('0x4e20'); // 20000
txp.priorityGasFee.should.equal('0x1388'); // 5000
const signatures = await keys[0].sign(clients[0].getRootPath(), txp);
clients[0].pushSignatures(txp, signatures, (err, txp) => {
should.not.exist(err);
Expand Down
15 changes: 8 additions & 7 deletions packages/bitcore-wallet-service/src/lib/chain/eth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import _ from 'lodash';
import { IWallet } from 'src/lib/model';
import { IAddress } from 'src/lib/model/address';
import { WalletService } from 'src/lib/server';
import { IChain } from '../../../types/chain';
import { Common } from '../../common';
import { ClientError } from '../../errors/clienterror';
import { Errors } from '../../errors/errordefinitions';
import logger from '../../logger';
import { ERC20Abi } from './abi-erc20';
import { InvoiceAbi } from './abi-invoice';
import type { IChain } from '../../../types/chain';
import type { TxProposal } from '../../model/txproposal';

const {
Constants,
Expand Down Expand Up @@ -98,24 +99,24 @@ export class EthChain implements IChain {
// getPendingTxs returns all txps when given a native currency
server.getPendingTxs(opts, (err, txps) => {
if (err) return cb(err);
let fees = 0;
let amounts = 0;
let fees = 0n;
let amounts = 0n;

txps.filter(txp => {
// Add gas used for tokens when getting native balance
if (!opts.tokenAddress) {
fees += txp.fee || 0;
fees += txp.fee ? BigInt(txp.fee) : 0n;
}
// Filter tokens when getting native balance
if (txp.tokenAddress && !opts.tokenAddress) {
return false;
}
amounts += txp.amount;
amounts += txp.amount ? BigInt(txp.amount) : 0n;
return true;
});

// TODO support big int
const lockedSum = (amounts + fees) || 0; // previously set to 0 if opts.multisigContractAddress
const lockedSum = Number(amounts + fees) || 0; // previously set to 0 if opts.multisigContractAddress
const convertedBalance = this.convertBitcoreBalance(balance, lockedSum);
server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => {
if (err) return cb(err);
Expand Down Expand Up @@ -286,7 +287,7 @@ export class EthChain implements IChain {
});
}

getBitcoreTx(txp, opts = { signed: true }) {
getBitcoreTx(txp: TxProposal, opts = { signed: true }) {
const {
data,
outputs,
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcore-wallet-service/src/lib/chain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class ChainProxy {
return this.get(wallet.chain).getFee(server, wallet, opts);
}

getBitcoreTx(txp: TxProposal, opts = { signed: true }) {
getBitcoreTx(txp: TxProposal<any>, opts = { signed: true }) {
return this.get(txp.chain).getBitcoreTx(txp, { signed: opts.signed });
}

Expand Down
Loading