diff --git a/packages/bitcore-cli/src/commands/transaction.ts b/packages/bitcore-cli/src/commands/transaction.ts index 02cab51d8e8..2cc186766c3 100755 --- a/packages/bitcore-cli/src/commands/transaction.ts +++ b/packages/bitcore-cli/src/commands/transaction.ts @@ -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}`); diff --git a/packages/bitcore-cli/src/commands/txproposals.ts b/packages/bitcore-cli/src/commands/txproposals.ts index 0c8cf9d37e8..54add869878 100755 --- a/packages/bitcore-cli/src/commands/txproposals.ts +++ b/packages/bitcore-cli/src/commands/txproposals.ts @@ -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}`); diff --git a/packages/bitcore-cli/src/utils.ts b/packages/bitcore-cli/src/utils.ts index 04570699c7c..1794194bf2a 100644 --- a/packages/bitcore-cli/src/utils.ts +++ b/packages/bitcore-cli/src/utils.ts @@ -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]); } diff --git a/packages/bitcore-cli/test/proposals.test.ts b/packages/bitcore-cli/test/proposals.test.ts index 958a0d46dea..def992e3715 100644 --- a/packages/bitcore-cli/test/proposals.test.ts +++ b/packages/bitcore-cli/test/proposals.test.ts @@ -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'; @@ -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) { diff --git a/packages/bitcore-wallet-client/.coveralls.yml b/packages/bitcore-wallet-client/.coveralls.yml deleted file mode 100644 index 8fa381ae6c0..00000000000 --- a/packages/bitcore-wallet-client/.coveralls.yml +++ /dev/null @@ -1,2 +0,0 @@ -repo_token: JrJM1SMeS0mtVEmRlgijo9LiovuFjVlLX - diff --git a/packages/bitcore-wallet-client/Gruntfile.js b/packages/bitcore-wallet-client/Gruntfile.js deleted file mode 100644 index bfe75ba54c3..00000000000 --- a/packages/bitcore-wallet-client/Gruntfile.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function(grunt) { - - // Project configuration. - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json') - }); - - // Default task(s). - grunt.registerTask('default', []); -}; diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index e77124ef11a..3497228c2d6 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -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); @@ -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 @@ -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(url, args); this._processTxps(txp); if (cb) { cb(null, txp); } @@ -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 @@ -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) { @@ -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(url, args); this._processTxps(signedTxp); @@ -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 { - $.checkState(this.credentials && this.credentials.isComplete(), - 'Failed state: this.credentials at '); - - const url = '/v1/txproposals/' + opts.txp.id + '/prepare/'; + async prepareTx(opts: { + txp: Txp; + }): Promise { + $.checkState(this.credentials?.isComplete(), 'Failed state: this.credentials at '); + + // 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(url, {}); this._processTxps(txp); return txp; @@ -4189,7 +4221,7 @@ export interface Txp { comment?: string; }>; // TODO addressType: string; - amount: number; + amount: number | string; chain: string; coin: string; changeAddress?: { @@ -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; inputs?: Array<{ address: string; - amount: number; + amount: number | string; confirmations: number; locked: boolean; path: string; publicKeys: Array; - satoshis: number; + satoshis: number | string; scriptPubKey: string; spent: boolean; txid: string; @@ -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; 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() diff --git a/packages/bitcore-wallet-client/src/lib/bulkclient.ts b/packages/bitcore-wallet-client/src/lib/bulkclient.ts index a7513d7aff7..fd0e136f951 100644 --- a/packages/bitcore-wallet-client/src/lib/bulkclient.ts +++ b/packages/bitcore-wallet-client/src/lib/bulkclient.ts @@ -94,6 +94,7 @@ export class BulkClient extends Request> { 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) { diff --git a/packages/bitcore-wallet-client/test/api.test.ts b/packages/bitcore-wallet-client/test/api.test.ts index c368925cf91..34376acc9b1 100644 --- a/packages/bitcore-wallet-client/test/api.test.ts +++ b/packages/bitcore-wallet-client/test/api.test.ts @@ -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; @@ -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'); @@ -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); } }); }); @@ -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'); @@ -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); @@ -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'); @@ -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); diff --git a/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts b/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts index 531783bd622..7296a662e96 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/eth/index.ts @@ -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, @@ -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); @@ -286,7 +287,7 @@ export class EthChain implements IChain { }); } - getBitcoreTx(txp, opts = { signed: true }) { + getBitcoreTx(txp: TxProposal, opts = { signed: true }) { const { data, outputs, diff --git a/packages/bitcore-wallet-service/src/lib/chain/index.ts b/packages/bitcore-wallet-service/src/lib/chain/index.ts index 0ab68fa815a..76995c274ae 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/index.ts @@ -94,7 +94,7 @@ class ChainProxy { return this.get(wallet.chain).getFee(server, wallet, opts); } - getBitcoreTx(txp: TxProposal, opts = { signed: true }) { + getBitcoreTx(txp: TxProposal, opts = { signed: true }) { return this.get(txp.chain).getBitcoreTx(txp, { signed: opts.signed }); } diff --git a/packages/bitcore-wallet-service/src/lib/chain/sol/index.ts b/packages/bitcore-wallet-service/src/lib/chain/sol/index.ts index 4e614457471..cb2e00f94f1 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/sol/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/sol/index.ts @@ -56,18 +56,18 @@ export class SolChain implements IChain { const { fees, amounts } = txps.reduce((acc, txp) => { // Add gas used for tokens when getting native balance if (!opts.tokenAddress) { - acc.fees += txp.fee || 0; + acc.fees += txp.fee ? BigInt(txp.fee) : 0n; } // Filter tokens when getting native balance if (!(txp.tokenAddress && !opts.tokenAddress)) { - acc.amounts += txp.amount; + acc.amounts += txp.amount ? BigInt(txp.amount) : 0n; } return acc; - }, { fees: 0, amounts: 0 }); + }, { fees: 0n, amounts: 0n }); - 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 reserveAmount = opts.tokenAddress ? 0 : reserve; const convertedBalance = this.convertBitcoreBalance(balance, lockedSum, reserveAmount); server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => { diff --git a/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts b/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts index 122d1f40142..deae334310f 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts @@ -65,9 +65,9 @@ export class XrpChain implements IChain { server.getPendingTxs(opts, (err, txps) => { if (err) return cb(err); const lockedSum = txps.reduce((sum, txp) => { - return sum + txp.amount + (txp.fee || 0); - }, 0) || 0; - const convertedBalance = this.convertBitcoreBalance(balance, lockedSum, reserve); + return sum + BigInt(txp.amount || 0) + BigInt(txp.fee || 0); + }, 0n) || 0n; + const convertedBalance = this.convertBitcoreBalance(balance, Number(lockedSum), reserve); server.storage.fetchAddresses(server.walletId, (err, addresses: IAddress[]) => { if (err) return cb(err); if (addresses.length > 0) { diff --git a/packages/bitcore-wallet-service/src/lib/errors/errordefinitions.ts b/packages/bitcore-wallet-service/src/lib/errors/errordefinitions.ts index 63be61554d5..43884042759 100644 --- a/packages/bitcore-wallet-service/src/lib/errors/errordefinitions.ts +++ b/packages/bitcore-wallet-service/src/lib/errors/errordefinitions.ts @@ -61,6 +61,7 @@ interface Errors { WALLET_NOT_FOUND: T; WALLET_NEED_SCAN: T; WRONG_SIGNING_METHOD: T; + INVALID_NUMBER_FORMAT: T; TSS_SESSION_NOT_FOUND: T; TSS_INVALID_PASSWORD: T; TSS_ROUND_ALREADY_DONE: T; @@ -132,6 +133,7 @@ const errors: Errors = { WALLET_NOT_FOUND: 'Wallet not found', WALLET_NEED_SCAN: 'Wallet needs addresses scan', WRONG_SIGNING_METHOD: 'Wrong signed method for coin/network', + INVALID_NUMBER_FORMAT: 'Invalid number format. Supported formats are: number, string, hex', TSS_SESSION_NOT_FOUND: 'Session not found', TSS_INVALID_PASSWORD: 'Invalid password', TSS_ROUND_ALREADY_DONE: 'Your message is for a round that has already finished', diff --git a/packages/bitcore-wallet-service/src/lib/expressapp.ts b/packages/bitcore-wallet-service/src/lib/expressapp.ts index 5f08c34b243..71f42d27bf2 100644 --- a/packages/bitcore-wallet-service/src/lib/expressapp.ts +++ b/packages/bitcore-wallet-service/src/lib/expressapp.ts @@ -49,7 +49,8 @@ export class ExpressApp { getServerWithAuth, getServerWithMultiAuth, logDeprecated, - setPublicCache + setPublicCache, + checkNumberFormat } = createRouteHelpers(returnError); registerWalletRoutes(router, { @@ -58,12 +59,14 @@ export class ExpressApp { getServerWithAuth, getServerWithMultiAuth, logDeprecated, - returnError + returnError, + checkNumberFormat }); registerTransactionRoutes(router, { getServerWithAuth, - returnError + returnError, + checkNumberFormat }); registerAdvertisementRoutes(router, { diff --git a/packages/bitcore-wallet-service/src/lib/model/txnote.ts b/packages/bitcore-wallet-service/src/lib/model/txnote.ts index da3ee1ea0da..ebcd532833f 100644 --- a/packages/bitcore-wallet-service/src/lib/model/txnote.ts +++ b/packages/bitcore-wallet-service/src/lib/model/txnote.ts @@ -16,6 +16,9 @@ export class TxNote { editedOn: number; editedBy: string; + // Non-persisted fields - populated when fetching from storage + editedByName?: string; + static create(opts) { opts = opts || {}; diff --git a/packages/bitcore-wallet-service/src/lib/model/txproposal.ts b/packages/bitcore-wallet-service/src/lib/model/txproposal.ts index eb1764e2003..40f8f57542b 100644 --- a/packages/bitcore-wallet-service/src/lib/model/txproposal.ts +++ b/packages/bitcore-wallet-service/src/lib/model/txproposal.ts @@ -1,11 +1,13 @@ +import { Utils as CWCUtils } from '@bitpay-labs/crypto-wallet-core'; import _ from 'lodash'; import { singleton } from 'preconditions'; import Uuid from 'uuid'; import { ChainService } from '../chain/index'; import { Common } from '../common'; import logger from '../logger'; -import { IAddress } from './address'; -import { TxProposalLegacy } from './txproposal_legacy'; +import { type IAddress } from './address'; +import { type ITxNote, TxNote } from './txnote'; +import { type ITxProposalLegacy, TxProposalLegacy } from './txproposal_legacy'; import { TxProposalAction } from './txproposalaction'; const $ = singleton(); @@ -13,9 +15,9 @@ const { Constants, Defaults, Utils } = Common; type TxProposalStatus = 'temporary' | 'pending' | 'accepted' | 'rejected' | 'broadcasted'; -export interface ITxProposal { +export interface ITxProposal { + version: number; type?: string; - creatorName: string; createdOn: number; txid: string; txids?: Array; @@ -29,19 +31,19 @@ export interface ITxProposal { payProUrl?: string; from: string; sendMax?: boolean; - changeAddress?: IAddress; - escrowAddress?: IAddress; + changeAddress?: Partial; + escrowAddress?: Partial; inputs: any[]; outputs: Array<{ - amount: number; - address: string; + amount: NumberType; + address?: string; toAddress?: string; sourceAddress?: string; message?: string; data?: string; - gasLimit?: number; + gasLimit?: NumberType; script?: string; - satoshis?: number; + satoshis?: NumberType; tag?: number; }>; outputOrder: number[]; @@ -52,13 +54,12 @@ export interface ITxProposal { status: TxProposalStatus; actions: any[]; feeLevel: string; - feePerKb: number; + feePerKb: NumberType; excludeUnconfirmedUtxos: boolean; addressType: string; customData: any; - amount: string; - fee: number; - version: number; + amount: NumberType; + fee: NumberType; broadcastedOn: number; inputPaths: string | any[]; proposalSignature: string; @@ -66,41 +67,51 @@ export interface ITxProposal { proposalSignaturePubKeySig: string; signingMethod: string; lowFees?: boolean; - nonce?: number | string; + raw?: Array | string; + nonce?: NumberType | string; deferNonce?: boolean; - gasPrice?: number; - maxGasFee?: number; - priorityGasFee?: number; + gasPrice?: NumberType; + maxGasFee?: NumberType; + priorityGasFee?: NumberType; txType?: number | string; - gasLimit?: number; // Backward compatibility for BWC <= 8.9.0 + gasLimit?: NumberType; // Backward compatibility for BWC <= 8.9.0 data?: string; // Backward compatibility for BWC <= 8.9.0 tokenAddress?: string; multisigContractAddress?: string; + multisigTxId?: string; destinationTag?: string; invoiceID?: string; - lockUntilBlockHeight?: number; - instantAcceptanceEscrow?: number; + lockUntilBlockHeight?: NumberType; + instantAcceptanceEscrow?: NumberType; isTokenSwap?: boolean; + multiSendContractAddress?: string; enableRBF?: boolean; replaceTxByFee?: boolean; multiTx?: boolean; // proposal contains multiple transactions - space?: number; + space?: NumberType; nonceAddress?: string; blockHash?: string; - blockHeight?: number; + blockHeight?: NumberType; category?: string; - priorityFee?: number; - computeUnits?: number; + priorityFee?: NumberType; + computeUnits?: NumberType; memo?: string; fromAta?: string; decimals?: number; refreshOnPublish?: boolean; prePublishRaw?: string; + + // Non-persistent fields - populated on fetch + creatorName: string; + derivationStrategy?: string; + note?: ITxNote; } -export class TxProposal implements ITxProposal { +export type NumberFormat = 'hex' | 'number' | 'string' | 'bigint'; + +export class TxProposal implements ITxProposal { + version: number; type?: string; - creatorName: string; createdOn: number; id: string; txid: string; @@ -114,19 +125,19 @@ export class TxProposal implements ITxProposal { payProUrl?: string; from: string; sendMax?: boolean; - changeAddress?: IAddress; - escrowAddress?: IAddress; + changeAddress?: Partial; + escrowAddress?: Partial; inputs: any[]; outputs: Array<{ - amount: number; - address: string; + amount: NumberType; + address?: string; toAddress?: string; sourceAddress?: string; message?: string; data?: string; - gasLimit?: number; + gasLimit?: NumberType; script?: string; - satoshis?: number; + satoshis?: NumberType; tag?: number; }>; outputOrder: number[]; @@ -137,13 +148,12 @@ export class TxProposal implements ITxProposal { status: TxProposalStatus; actions: any[] = []; feeLevel: string; - feePerKb: number; + feePerKb: NumberType; excludeUnconfirmedUtxos: boolean; addressType: string; customData: any; - amount: string; - fee: number; - version: number; + amount: NumberType; + fee: NumberType; broadcastedOn: number; inputPaths: string | any[]; proposalSignature: string; @@ -152,38 +162,43 @@ export class TxProposal implements ITxProposal { signingMethod: string; lowFees?: boolean; raw?: Array | string; - nonce?: number | string; + nonce?: NumberType; deferNonce?: boolean; - gasPrice?: number; - maxGasFee?: number; - priorityGasFee?: number; + gasPrice?: NumberType; + maxGasFee?: NumberType; + priorityGasFee?: NumberType; txType?: number | string; - gasLimit?: number; // Backward compatibility for BWC <= 8.9.0 + gasLimit?: NumberType; // Backward compatibility for BWC <= 8.9.0 data?: string; // Backward compatibility for BWC <= 8.9.0 tokenAddress?: string; multisigContractAddress?: string; multisigTxId?: string; destinationTag?: string; invoiceID?: string; - lockUntilBlockHeight?: number; - instantAcceptanceEscrow?: number; + lockUntilBlockHeight?: NumberType; + instantAcceptanceEscrow?: NumberType; isTokenSwap?: boolean; multiSendContractAddress?: string; enableRBF?: boolean; replaceTxByFee?: boolean; multiTx?: boolean; - space?: number; + space?: NumberType; nonceAddress?: string; blockHash?: string; - blockHeight?: number; + blockHeight?: NumberType; category?: string; - priorityFee?: number; - computeUnits?: number; + priorityFee?: NumberType; + computeUnits?: NumberType; memo?: string; fromAta?: string; decimals?: number; refreshOnPublish?: boolean; prePublishRaw?: string; + + // Non-persistent fields - populated on fetch + creatorName: string; + derivationStrategy?: string; + note?: TxNote; static create(opts) { opts = opts || {}; @@ -308,6 +323,8 @@ export class TxProposal implements ITxProposal { return x; } + static fromObj(obj: Partial): TxProposal; + static fromObj(obj: Partial): TxProposalLegacy; static fromObj(obj) { if (!(obj.version >= 3)) { return TxProposalLegacy.fromObj(obj); @@ -320,6 +337,7 @@ export class TxProposal implements ITxProposal { x.id = obj.id; x.walletId = obj.walletId; x.creatorId = obj.creatorId; + x.creatorName = obj.creatorName; x.coin = obj.coin || Defaults.COIN; x.chain = obj.chain?.toLowerCase() || Utils.getChain(x.coin); // getChain -> backwards compatibility x.network = obj.network; @@ -327,6 +345,7 @@ export class TxProposal implements ITxProposal { x.amount = obj.amount; x.message = obj.message; x.payProUrl = obj.payProUrl; + x.derivationStrategy = obj.derivationStrategy; x.sendMax = obj.sendMax; x.changeAddress = obj.changeAddress; x.escrowAddress = obj.escrowAddress; @@ -341,9 +360,7 @@ export class TxProposal implements ITxProposal { x.txids = obj.txids; x.broadcastedOn = obj.broadcastedOn; x.inputPaths = obj.inputPaths; - x.actions = _.map(obj.actions, action => { - return TxProposalAction.fromObj(action); - }); + x.actions = (obj.actions || []).map(action => TxProposalAction.fromObj(action)); x.outputOrder = obj.outputOrder; x.fee = obj.fee; x.feeLevel = obj.feeLevel; @@ -414,7 +431,7 @@ export class TxProposal implements ITxProposal { setInputs(inputs) { this.inputs = inputs || []; - this.inputPaths = _.map(inputs, 'path') || []; + this.inputPaths = this.inputs.map(input => input.path); } _updateStatus() { @@ -444,8 +461,10 @@ export class TxProposal implements ITxProposal { return signatures; } - getRawTx() { - const t = ChainService.getBitcoreTx(this); + getRawTx(numberFormat?: NumberFormat) { + // Casting numberFormat to 'number' is to sidestep TS errors, but is not necessarily true. + const txp = numberFormat ? TxProposal.formatNumbers(this, numberFormat as 'number') : this as TxProposal; + const t = ChainService.getBitcoreTx(txp); return t.uncheckedSerialize(); } @@ -455,7 +474,7 @@ export class TxProposal implements ITxProposal { * @return {Number} total amount of all outputs excluding change output */ getTotalAmount() { - return Number((this.outputs || []).reduce((total, o) => total += BigInt(o.amount), 0n)); + return Number(((this as TxProposal).outputs || []).reduce((total, o) => total += BigInt(o.amount), 0n)); } /** @@ -464,7 +483,7 @@ export class TxProposal implements ITxProposal { * @return {String[]} copayerIds that performed actions in this proposal (accept / reject) */ getActors() { - return _.map(this.actions, 'copayerId'); + return (this.actions || []).map(a => a.copayerId); } /** @@ -473,12 +492,9 @@ export class TxProposal implements ITxProposal { * @return {String[]} copayerIds that approved the tx proposal (accept) */ getApprovers() { - return _.map( - _.filter(this.actions, a => { - return a.type == 'accept'; - }), - 'copayerId' - ); + return (this.actions || []) + .filter(a => a.type == 'accept') + .map(a => a.copayerId); } /** @@ -488,9 +504,7 @@ export class TxProposal implements ITxProposal { * @return {Object} type / createdOn */ getActionBy(copayerId) { - return _.find(this.actions, { - copayerId - }); + return (this.actions || []).find(a => a.copayerId == copayerId); } addAction(copayerId, type, comment, signatures?, xpub?) { @@ -505,10 +519,11 @@ export class TxProposal implements ITxProposal { this._updateStatus(); } - sign(copayerId, signatures, xpub) { + sign(copayerId, signatures, xpub, numberFormat?: NumberFormat) { try { - // Tests signatures are OK - const tx = ChainService.getBitcoreTx(this); + // numberFormat as 'number' is to sidestep TS errors, but is not necessarily true. + const txp = numberFormat ? TxProposal.formatNumbers(this, numberFormat as 'number') : this as TxProposal; + const tx = ChainService.getBitcoreTx(txp); ChainService.addSignaturesToBitcoreTx( this.chain, tx, @@ -574,4 +589,70 @@ export class TxProposal implements ITxProposal { this.status = 'broadcasted'; this.broadcastedOn = Math.floor(Date.now() / 1000); } + + + /** + * Replaces tx-building number values with the specified number format. + * This is to ensure consistency across BWC and BWS when handling large numbers, especially for chains like ETH and SOL. + * @param {TxProposal|ITxProposal} txp Transaction Proposal + * @param {'number'|'string'|'bigint'|'hex'} numberFormat The desired number format for the tx-building values. Can be 'number', 'string', 'bigint', or 'hex'. + */ + static formatNumbers(txp: TxProposal, numberFormat: 'string'): TxProposal; + static formatNumbers(txp: TxProposal, numberFormat: 'hex'): TxProposal; + static formatNumbers(txp: TxProposal, numberFormat: 'bigint'): TxProposal; + static formatNumbers(txp: TxProposal, numberFormat: 'number'): TxProposal; + static formatNumbers(txp: ITxProposal, numberFormat: 'string'): ITxProposal; + static formatNumbers(txp: ITxProposal, numberFormat: 'hex'): ITxProposal; + static formatNumbers(txp: ITxProposal, numberFormat: 'bigint'): ITxProposal; + static formatNumbers(txp: ITxProposal, numberFormat: 'number'): ITxProposal; + static formatNumbers(txp: TxProposal | ITxProposal, numberFormat: NumberFormat = 'number') { + let convertFn; + switch (numberFormat) { + case 'number': + convertFn = parseInt; + break; + case 'string': + convertFn = (n) => typeof n === 'string' && n.startsWith('0x') ? BigInt(n).toString() : n.toString(); + break; + case 'hex': + convertFn = (n) => CWCUtils.toHex(n); + break; + case 'bigint': + convertFn = (n) => BigInt(n); + break; + default: + logger.warn(`Invalid numberFormat: ${numberFormat}, no conversion will be applied to tx proposal ${txp.id}`); + return txp; + } + + const primitiveTypes = new Set(['number', 'string', 'bigint']); + const convert = (key, value) => { + if ((numberFormat === 'hex' || typeof value !== numberFormat) && primitiveTypes.has(typeof value)) { + try { + value = convertFn(value); + } catch (e) { + logger.warn(`Failed to convert ${txp.id} > ${key} with value ${value} to ${numberFormat}: ${e.message}`); + } + } + return value; + }; + + const _txp = txp instanceof TxProposal ? TxProposal.fromObj(txp.toObject()) : TxProposal.fromObj(txp as ITxProposal).toObject(); + + const topKeys = ['amount', 'feePerKb', 'fee', 'nonce', 'gasPrice', 'maxGasFee', 'priorityGasFee', 'gasLimit', 'lockUntilBlockHeight', 'instantAcceptanceEscrow', 'space', 'blockHeight', 'computeUnits', 'decimals']; + for (const key of topKeys) { + const value = _txp[key]; + _txp[key] = convert(key, value); + } + + const outputKeys = ['amount', 'gasLimit', 'satoshis']; + for (let i = 0; i < _txp.outputs.length; i++) { + for (const key of outputKeys) { + const value = _txp.outputs[i][key]; + _txp.outputs[i][key] = convert(`output.${i}.${key}`, value); + } + } + + return _txp; + } } diff --git a/packages/bitcore-wallet-service/src/lib/model/txproposal_legacy.ts b/packages/bitcore-wallet-service/src/lib/model/txproposal_legacy.ts index 8925a006511..3991c4af89f 100644 --- a/packages/bitcore-wallet-service/src/lib/model/txproposal_legacy.ts +++ b/packages/bitcore-wallet-service/src/lib/model/txproposal_legacy.ts @@ -15,7 +15,7 @@ function throwUnsupportedError() { throw new Error(msg); } -export interface ITxProposal { +export interface ITxProposalLegacy { version: string; type: string; createdOn: number; @@ -35,7 +35,7 @@ export interface ITxProposal { walletN: number; status: string; txid: string; - broadcastedOn: string; + broadcastedOn: number; inputPaths: string; actions: any[]; outputOrder: number; @@ -49,8 +49,9 @@ export interface ITxProposal { addressType: string; derivationStrategy: string; customData: any; -} -export class TxProposalLegacy { +}; + +export class TxProposalLegacy implements ITxProposalLegacy { version: string; type: string; createdOn: number; diff --git a/packages/bitcore-wallet-service/src/lib/model/txproposalaction.ts b/packages/bitcore-wallet-service/src/lib/model/txproposalaction.ts index 5f1d29052d2..7e30fd14258 100644 --- a/packages/bitcore-wallet-service/src/lib/model/txproposalaction.ts +++ b/packages/bitcore-wallet-service/src/lib/model/txproposalaction.ts @@ -6,6 +6,9 @@ export interface ITxProposalAction { signatures: string[]; xpub: string; comment: string; + + // Non-persistent fields + copayerName?: string; } export class TxProposalAction { version: string; @@ -16,6 +19,9 @@ export class TxProposalAction { xpub: string; comment: string; + // Non-persistent fields + copayerName?: string; + static create(opts) { opts = opts || {}; @@ -43,6 +49,9 @@ export class TxProposalAction { x.xpub = obj.xpub; x.comment = obj.comment; + // copayerName is not stored in the actions collection, but it is returned by the server on fetchTxProposal, so we need to set it here. + x.copayerName = obj.copayerName; + return x; } } diff --git a/packages/bitcore-wallet-service/src/lib/routes/context.ts b/packages/bitcore-wallet-service/src/lib/routes/context.ts index 9305a7f28b9..e00cff65555 100644 --- a/packages/bitcore-wallet-service/src/lib/routes/context.ts +++ b/packages/bitcore-wallet-service/src/lib/routes/context.ts @@ -10,6 +10,7 @@ interface RouteHelpers { getServerWithMultiAuth: Types.GetServerWithMultiAuthFn; logDeprecated: Types.LogDeprecatedFn; setPublicCache: (res: express.Response, seconds: number) => void; + checkNumberFormat: Types.CheckNumberFormatFn; } export function createRouteHelpers(returnError: Types.ReturnErrorFn): RouteHelpers { @@ -139,11 +140,19 @@ export function createRouteHelpers(returnError: Types.ReturnErrorFn): RouteHelpe res.setHeader('Cache-Control', `public, max-age=${seconds}, stale-if-error=${10 * seconds}`); }; + const checkNumberFormat = (numberFormat: string | undefined, res: express.Response) => { + const validFormats = ['hex', 'string', 'number']; // bigint cannot be serialized to JSON, so it's not supported in the API response + if (numberFormat && !validFormats.includes(numberFormat)) { + return returnError(Errors.INVALID_NUMBER_FORMAT, res, null); + } + }; + return { getServer, getServerWithAuth, getServerWithMultiAuth, logDeprecated, - setPublicCache + setPublicCache, + checkNumberFormat }; } diff --git a/packages/bitcore-wallet-service/src/lib/routes/transactions.ts b/packages/bitcore-wallet-service/src/lib/routes/transactions.ts index 230ef911430..2985e72d931 100644 --- a/packages/bitcore-wallet-service/src/lib/routes/transactions.ts +++ b/packages/bitcore-wallet-service/src/lib/routes/transactions.ts @@ -5,10 +5,11 @@ import type * as Types from '../../types/expressapp'; interface RouteContext { getServerWithAuth: Types.GetServerWithAuthFn; returnError: Types.ReturnErrorFn; + checkNumberFormat: Types.CheckNumberFormatFn; } export function registerTransactionRoutes(router: express.Router, context: RouteContext) { - const { getServerWithAuth, returnError } = context; + const { getServerWithAuth, returnError, checkNumberFormat } = context; router.get('/v1/txproposals/', (req, res) => { getServerWithAuth(req, res, server => { @@ -21,7 +22,13 @@ export function registerTransactionRoutes(router: express.Router, context: Route router.get('/v2/txproposals/', (req, res) => { getServerWithAuth(req, res, server => { - server.getPendingTxs({}, (err, pendings) => { + checkNumberFormat(req.query.numberFormat, res); + + const opts = { + numberFormat: req.query.numberFormat, + }; + + server.getPendingTxs(opts, (err, pendings) => { if (err) return returnError(err, res, req); res.json(pendings); }); @@ -45,7 +52,9 @@ export function registerTransactionRoutes(router: express.Router, context: Route router.post('/v3/txproposals/', (req, res) => { getServerWithAuth(req, res, server => { + checkNumberFormat(req.query.numberFormat, res); req.body.txpVersion = 3; + req.body.numberFormat = req.query.numberFormat; server.createTx(req.body, (err, txp) => { if (err) return returnError(err, res, req); res.json(txp); @@ -67,9 +76,11 @@ export function registerTransactionRoutes(router: express.Router, context: Route router.post('/v2/txproposals/:id/signatures/', (req, res) => { getServerWithAuth(req, res, server => { + checkNumberFormat(req.query.numberFormat, res); req.body.txProposalId = req.params['id']; req.body.maxTxpVersion = 3; req.body.supportBchSchnorr = true; + req.body.numberFormat = req.query.numberFormat; server.signTx(req.body, (err, txp) => { if (err) return returnError(err, res, req); res.json(txp); @@ -80,7 +91,9 @@ export function registerTransactionRoutes(router: express.Router, context: Route router.post('/v1/txproposals/:id/prepare/', (req, res) => { getServerWithAuth(req, res, server => { + checkNumberFormat(req.query.numberFormat, res); req.body.txProposalId = req.params['id']; + req.body.numberFormat = req.query.numberFormat; server.prepareTx(req.body, (err, txp) => { if (err) return returnError(err, res, req); res.json(txp); @@ -103,7 +116,9 @@ export function registerTransactionRoutes(router: express.Router, context: Route router.post('/v2/txproposals/:id/publish/', (req, res) => { getServerWithAuth(req, res, server => { + checkNumberFormat(req.query.numberFormat, res); req.body.txProposalId = req.params['id']; + req.body.numberFormat = req.query.numberFormat; server.publishTx(req.body, (err, txp) => { if (err) return returnError(err, res, req); res.json(txp); diff --git a/packages/bitcore-wallet-service/src/lib/routes/wallets.ts b/packages/bitcore-wallet-service/src/lib/routes/wallets.ts index 8bc2a38ff26..2e6ffa0029a 100644 --- a/packages/bitcore-wallet-service/src/lib/routes/wallets.ts +++ b/packages/bitcore-wallet-service/src/lib/routes/wallets.ts @@ -15,6 +15,7 @@ interface RouteContext { getServerWithMultiAuth: Types.GetServerWithMultiAuthFn; logDeprecated: Types.LogDeprecatedFn; returnError: Types.ReturnErrorFn; + checkNumberFormat: Types.CheckNumberFormatFn; } function getServerOrReturnError(req, res, context: RouteContext): WalletService | null { @@ -27,7 +28,7 @@ function getServerOrReturnError(req, res, context: RouteContext): WalletService } export function registerWalletRoutes(router: express.Router, context: RouteContext) { - const { createWalletLimiter, getServerWithAuth, getServerWithMultiAuth, logDeprecated, returnError } = context; + const { createWalletLimiter, getServerWithAuth, getServerWithMultiAuth, logDeprecated, returnError, checkNumberFormat } = context; router.post('/v1/wallets/', createWalletLimiter, (req, res) => { logDeprecated(req); @@ -96,13 +97,15 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte router.get('/v3/wallets/', (req, res) => { getServerWithAuth(req, res, server => { + checkNumberFormat(req.query.numberFormat, res); const opts = { includeExtendedInfo: false, twoStep: false, includeServerMessages: false, tokenAddress: req.query.tokenAddress, multisigContractAddress: req.query.multisigContractAddress, - network: req.query.network + network: req.query.network, + numberFormat: req.query.numberFormat }; if (req.query.includeExtendedInfo == '1') opts.includeExtendedInfo = true; if (req.query.twoStep == '1') opts.twoStep = true; @@ -121,6 +124,8 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte const twoStep = req.query.twoStep == '1'; const silentFailure = req.query.silentFailure == '1'; const includeServerMessages = req.query.serverMessageArray == '1'; + const numberFormat = req.query.numberFormat; + checkNumberFormat(numberFormat, res); const buildOpts = (request, copayerId) => { const getParam = (param, returnArray = false) => { @@ -136,6 +141,7 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte twoStep, silentFailure, includeServerMessages, + numberFormat, tokenAddresses: getParam('tokenAddress', true) as string[] | null, multisigContractAddress: getParam('multisigContractAddress') as string | null, network: getParam('network') as string | null diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index 51a55876d6d..1b21aed22b1 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -844,6 +844,7 @@ export class WalletService implements IWalletService { * @param {Object} opts.tokenAddress - (Optional) Token contract address to pass in getBalance * @param {Object} opts.multisigContractAddress - (Optional) Multisig ETH contract address to pass in getBalance * @param {Object} opts.network - (Optional ETH MULTISIG) Multisig ETH contract address network + * @param {string} opts.numberFormat Returns txp tx-build values as 'string', 'hex', or 'number' (default) * @returns {Object} status */ getStatus(opts, cb) { @@ -1879,7 +1880,7 @@ export class WalletService implements IWalletService { next => { utxoIndex = _.keyBy(allUtxos, utxoKey); - this.getPendingTxs({}, (err, txps) => { + this.getPendingTxs({ numberFormat: opts.numberFormat }, (err, txps) => { if (err) return next(err); const lockedInputs = txps.flatMap(t => t.inputs).map(utxoKey); @@ -1888,7 +1889,7 @@ export class WalletService implements IWalletService { utxoIndex[input].locked = true; } } - logger.debug(`Got ${lockedInputs.length} locked utxos`); + logger.debug(`Got ${lockedInputs.length} locked utxos`); return next(); }); }, @@ -2689,6 +2690,17 @@ export class WalletService implements IWalletService { }); } + createTx(opts, cb) { + this._createTx(opts, (err, txp) => { + if (err) return cb(err); + if (opts.numberFormat) { + txp = TxProposal.formatNumbers(txp, opts.numberFormat); + } + return cb(null, txp); + }); + } + + /** * Creates a new transaction proposal. * @param {Object} opts @@ -2736,7 +2748,7 @@ export class WalletService implements IWalletService { * @param {Boolean} opts.refreshOnPublish - Optional. Allows publish function to refresh txp data * @returns {TxProposal} Transaction proposal. outputs address format will use the same format as inpunt. */ - createTx(opts, cb) { + private _createTx(opts, cb) { opts = opts ? _.clone(opts) : {}; const checkTxpAlreadyExists = (txProposalId, cb) => { @@ -3001,6 +3013,17 @@ export class WalletService implements IWalletService { ); } + + publishTx(opts, cb) { + this._publishTx(opts, (err, txp) => { + if (err) return cb(err); + if (opts.numberFormat) { + txp = TxProposal.formatNumbers(txp, opts.numberFormat); + } + return cb(null, txp); + }); + } + /** * Publish an already created tx proposal so inputs are locked and other copayers in the wallet can see it. * @param {Object} opts @@ -3008,7 +3031,7 @@ export class WalletService implements IWalletService { * @param {string} opts.proposalSignature - S(raw tx). Used by other copayers to verify the proposal. * @param {Boolean} [opts.noCashAddr] - do not use cashaddress for bch */ - publishTx(opts, cb) { + private _publishTx(opts, cb) { if (!checkRequired(opts, ['txProposalId', 'proposalSignature'], cb)) return; this._runLocked(cb, cb => { @@ -3089,7 +3112,7 @@ export class WalletService implements IWalletService { * @param {string} opts.txProposalId - The tx proposal id. * @returns {Object} txProposal */ - getTx(opts, cb) { + getTx(opts, cb: (err: any, txp?: TxProposal) => void) { this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => { if (err) return cb(err); if (!txp) return cb(Errors.TX_NOT_FOUND); @@ -3288,6 +3311,7 @@ export class WalletService implements IWalletService { * @param {string} opts.signatures - The signatures of the inputs of this tx for this copayer (in appearance order) * @param {string} opts.maxTxpVersion - Client's maximum supported txp version * @param {boolean} opts.supportBchSchnorr - indication whether to use schnorr for signing tx + * @param {string} opts.numberFormat - Optional. If specified, the tx proposal will ensure the numbers are in the format specified (e.g. 'string'). This is to ensure precision handling matches the client's */ signTx(opts, cb) { if (!checkRequired(opts, ['txProposalId', 'signatures'], cb)) return; @@ -3344,7 +3368,7 @@ export class WalletService implements IWalletService { const xPubKey = wallet.tssKeyId ? wallet.clientDerivedPublicKey : copayer.xPubKey; try { - if (!txp.sign(this.copayerId, opts.signatures, xPubKey)) { + if (!txp.sign(this.copayerId, opts.signatures, xPubKey, opts.numberFormat)) { this.logw('Error signing transaction (BAD_SIGNATURES)'); this.logw('Client version:', this.clientVersion); this.logw('Arguments:', JSON.stringify(opts)); @@ -3414,6 +3438,16 @@ export class WalletService implements IWalletService { }); } + prepareTx(opts, cb) { + this._prepareTx(opts, (err, txp) => { + if (err) return cb(err); + if (opts.numberFormat) { + txp = TxProposal.formatNumbers(txp, opts.numberFormat); + } + return cb(null, txp); + }); + } + /** * Prepare a transaction proposal for signing. * Assigns JIT values (nonce, and in the future: fee, gas) to a deferred txp. @@ -3421,7 +3455,7 @@ export class WalletService implements IWalletService { * @param {Object} opts * @param {string} opts.txProposalId - The identifier of the transaction. */ - prepareTx(opts, cb) { + private _prepareTx(opts, cb) { if (!checkRequired(opts, ['txProposalId'], cb)) return; this._runLocked(cb, cb => { @@ -3636,16 +3670,34 @@ export class WalletService implements IWalletService { ); } + + /** + * Retrieves pending transaction proposals. + * @param {'number'|'string'|'hex'|'bigint'} opts.numberFormat The format to return numbers in. Not ALL numbers - just certain ones that are needed for tx building/verifying by the client. + * @param cb + */ + async getPendingTxs(opts, cb) { + this._getPendingTxs(opts, (err, txps) => { + if (err) return cb(err); + if (opts.numberFormat) { + const formattedTxps = txps.map(t => TxProposal.formatNumbers(t, opts.numberFormat)); + return cb(null, formattedTxps); + } + return cb(null, txps); + }); + } + /** * Retrieves pending transaction proposals. * @param {Object} opts * @param {Boolean} opts.noCashAddr (do not use cashaddr, only for backwards compat) * @param {String} opts.tokenAddress ERC20 Token Contract Address * @param {String} opts.multisigContractAddress MULTISIG ETH Contract Address - * @param {String} opts.network The network of the MULTISIG ETH transactions + * @param {String} opts.network The network of the MULTISIG ETH transactions + * @param {String} opts.numberFormat Return numbers as format. 'number' (default), 'hex', or 'string' * @returns {TxProposal[]} Transaction proposal. */ - async getPendingTxs(opts, cb) { + private async _getPendingTxs(opts, cb: (err: any, txps?: ITxProposal[]) => void) { if (opts.multisigContractAddress) { try { const multisigTxpsInfo = await this.getMultisigTxpsInfo(opts); diff --git a/packages/bitcore-wallet-service/src/lib/storage.ts b/packages/bitcore-wallet-service/src/lib/storage.ts index 0c6b3787aff..8e10d308a69 100644 --- a/packages/bitcore-wallet-service/src/lib/storage.ts +++ b/packages/bitcore-wallet-service/src/lib/storage.ts @@ -310,24 +310,25 @@ export class Storage { } // TODO: should be done client-side - _completeTxData(walletId, txs, cb) { + _completeTxData(walletId: string, txs: TxProposal | TxProposal[], cb: (err?: any, txs?: TxProposal | TxProposal[]) => void) { this.fetchWallet(walletId, (err, wallet) => { if (err) return cb(err); - _.each([].concat(txs), tx => { + for (const tx of [].concat(txs) as TxProposal[]) { tx.derivationStrategy = wallet.derivationStrategy || 'BIP45'; tx.creatorName = wallet.getCopayer(tx.creatorId).name; - _.each(tx.actions, action => { + for (const action of tx.actions) { action.copayerName = wallet.getCopayer(action.copayerId).name; - }); + } - if (tx.status == 'accepted') tx.raw = tx.getRawTx(); - }); + if (tx.status == 'accepted') + tx.raw = tx.getRawTx(); + } return cb(null, txs); }); } // TODO: remove walletId from signature - fetchTx(walletId, txProposalId, cb) { + fetchTx(walletId: string, txProposalId: string, cb: (err?: any, tx?: TxProposal) => void) { if (!this.db) return cb(); this.db.collection(collections.TXS).findOne( @@ -385,7 +386,7 @@ export class Storage { } fetchEthPendingTxs(multisigTxpsInfo) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.db .collection(collections.TXS) .find({ @@ -1286,7 +1287,7 @@ export class Storage { }); } - fetchTxNote(walletId, txid, cb) { + fetchTxNote(walletId, txid, cb: (err?: any, note?: TxNote) => void) { this.db.collection(collections.TX_NOTES).findOne( { walletId, @@ -1301,12 +1302,12 @@ export class Storage { } // TODO: should be done client-side - _completeTxNotesData(walletId, notes, cb) { + _completeTxNotesData(walletId, notes: TxNote | TxNote[], cb: (err?: any, notes?: TxNote | TxNote[]) => void) { this.fetchWallet(walletId, (err, wallet) => { if (err) return cb(err); - _.each([].concat(notes), note => { + for (const note of [].concat(notes) as TxNote[]) { note.editedByName = wallet.getCopayer(note.editedBy).name; - }); + } return cb(null, notes); }); } diff --git a/packages/bitcore-wallet-service/src/types/expressapp.d.ts b/packages/bitcore-wallet-service/src/types/expressapp.d.ts index 12dd0ee4070..60ee6215e68 100644 --- a/packages/bitcore-wallet-service/src/types/expressapp.d.ts +++ b/packages/bitcore-wallet-service/src/types/expressapp.d.ts @@ -12,4 +12,5 @@ export type GetServerFn = (req: express.Request, res: express.Response) => Walle export type ServerCallback = (server: WalletService, err?: Error) => void; export type GetServerWithAuthFn = (req: express.Request, res: express.Response, opts?: ServerOpts | ServerCallback, cb?: ServerCallback) => Promise; export type GetServerWithMultiAuthFn = (req: express.Request, res: express.Response, opts?: ServerOpts) => Array>; -export type CreateWalletLimiterFn = (req: express.Request, res: express.Response, next: express.NextFunction) => void; \ No newline at end of file +export type CreateWalletLimiterFn = (req: express.Request, res: express.Response, next: express.NextFunction) => void; +export type CheckNumberFormatFn = (numberFormat: string | undefined, res: express.Response) => void; \ No newline at end of file diff --git a/packages/bitcore-wallet-service/test/chain/bch.test.ts b/packages/bitcore-wallet-service/test/chain/bch.test.ts index e1778659789..8b19bd0d66c 100644 --- a/packages/bitcore-wallet-service/test/chain/bch.test.ts +++ b/packages/bitcore-wallet-service/test/chain/bch.test.ts @@ -73,6 +73,11 @@ const aTXP = function() { version: '1.0.0', createdOn: 1424372337, address: '3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH', + walletId: 'thisdoesntmatter', + coin: 'bch', + chain: 'bch', + network: 'livenet', + type: 'P2SH', path: 'm/2147483647/1/0', publicKeys: ['030562cb099e6043dc499eb359dd97c9d500a3586498e4bcf0228a178cc20e6f16', '0367027d17dbdfc27b5e31f8ed70e14d47949f0fa392261e977db0851c8b0d6fac', @@ -96,15 +101,17 @@ const aTXP = function() { requiredRejections: 1, walletN: 2, addressType: 'P2SH', - status: 'pending', + status: 'pending' as const, actions: [], fee: 10000, outputs: [{ toAddress: 'qq0zkyfjkjfec47hgcu0acdzw36td4s2evqyyllszk', + address: 'qq0zkyfjkjfec47hgcu0acdzw36td4s2evqyyllszk', amount: 10000000, message: 'first message' }, { toAddress: 'qq0zkyfjkjfec47hgcu0acdzw36td4s2evqyyllszk', + address: 'qq0zkyfjkjfec47hgcu0acdzw36td4s2evqyyllszk', amount: 20000000, message: 'second message' }], diff --git a/packages/bitcore-wallet-service/test/chain/btc.test.ts b/packages/bitcore-wallet-service/test/chain/btc.test.ts index 4512d6419cd..78795982847 100644 --- a/packages/bitcore-wallet-service/test/chain/btc.test.ts +++ b/packages/bitcore-wallet-service/test/chain/btc.test.ts @@ -262,7 +262,7 @@ const aTXP = function() { chain: 'btc', coin: 'btc', network: 'livenet', - amount: '30000000', + amount: 30000000, message: 'some message', proposalSignature: '7035022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9', changeAddress: { @@ -328,4 +328,4 @@ const aTXP = function() { return txp; }; -const signedTxp = { 'actions': [{ 'version': '1.0.0', 'createdOn': 1588099987, 'copayerId': '671fee02a6c1c4de2e2609f9f9a6180dc03acfff6b759fe0b13a616ed4880065', 'type': 'accept', 'signatures': ['3045022100ace0efb22eaf21b77d5e4a31a491ad06fbcfe700ace5abe8453ec0328c714cba02205090a44975d4b65b5e2f4473da038e83875826b369fc79558a741268ed574bb0'], 'xpub': 'xpub6DVmxcjRgZdHNSEcXSiFtweVwMSTc3TMwRJ45nJYvyqvLbK1poPerupqh87rSoz27wvckb1CKnGZoLmLXSZyNGZtVd7neqSvdwJL6fceQpe', 'comment': null }], 'version': 3, 'createdOn': 1588099987, 'id': '7cea0d95-3308-48e7-a4be-3f16d66e1f5a', 'walletId': '7eaf4d32-c2fd-4262-864a-4c42fc9236f8', 'creatorId': '671fee02a6c1c4de2e2609f9f9a6180dc03acfff6b759fe0b13a616ed4880065', 'coin': 'bch', 'network': 'livenet', 'outputs': [{ 'amount': 80000000, 'toAddress': 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X' }], 'amount': 80000000, 'message': 'some message', 'payProUrl': null, 'changeAddress': { 'version': '1.0.0', 'createdOn': 1588099987, 'address': 'CWwtFMy3GMr5qMEtvEdUDjePfShzkJXCnh', 'walletId': '7eaf4d32-c2fd-4262-864a-4c42fc9236f8', 'isChange': true, 'path': 'm/1/0', 'publicKeys': ['02129acdcc600694b3ce55a2d05244186e806174eb0bafde20e5a6395ded647857'], 'coin': 'bch', 'network': 'livenet', 'type': 'P2PKH', 'hasActivity': null, 'beRegistered': null }, 'inputs': [{ 'txid': '4e7feb01fb039f3a719e9390c8f7cb501fb8a0a083f72a43b4cbb420d8505cc0', 'vout': 5, 'satoshis': 100000000, 'scriptPubKey': '76a914d391e62337ed194a1e428f32d14838e5d848180a88ac', 'address': 'qrfere3rxlk3jjs7g28n952g8rjasjqcpgx3axq70t', 'confirmations': 44, 'publicKeys': ['024d27ca79a3ed27a143cb9d1dff01e4e6445294679a700ca404ca449211d08aa7'], 'wallet': '7eaf4d32-c2fd-4262-864a-4c42fc9236f8', 'path': 'm/0/1' }], 'walletM': 1, 'walletN': 1, 'requiredSignatures': 1, 'requiredRejections': 1, 'status': 'accepted', 'txid': '8541ed35ac43e07e362a00ebab9448eefa63840c75ca38edff6785c223503a29', 'broadcastedOn': null, 'inputPaths': ['m/0/1'], 'outputOrder': [1, 0], 'fee': 2430, 'feeLevel': null, 'feePerKb': 10000, 'excludeUnconfirmedUtxos': false, 'addressType': 'P2PKH', 'customData': null, 'proposalSignature': '3045022100e8a1ac6eef882fb3e7311c725093317df99e5ed46f52fdee9064560bb757d1c902200b09f806a17d4c7950097b8714293725ce741c6de25914f81a8ca0ca1107fc4c', 'signingMethod': 'ecdsa', 'proposalSignaturePubKey': null, 'proposalSignaturePubKeySig': null, 'lockUntilBlockHeight': null, 'gasPrice': null, 'from': null, 'nonce': null, 'gasLimit': null, 'data': null, 'tokenAddress': null, 'destinationTag': null, 'invoiceID': null, 'derivationStrategy': 'BIP44', 'creatorName': 'copayer 1', 'raw': '0100000001c05c50d820b4cbb4432af783a0a0b81f50cbf7c890939e713a9f03fb01eb7f4e050000006b483045022100ace0efb22eaf21b77d5e4a31a491ad06fbcfe700ace5abe8453ec0328c714cba02205090a44975d4b65b5e2f4473da038e83875826b369fc79558a741268ed574bb04121024d27ca79a3ed27a143cb9d1dff01e4e6445294679a700ca404ca449211d08aa7ffffffff0282233101000000001976a9149edd2399faccf4e57df08bef78962fa0228741cf88ac00b4c404000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac00000000', 'isPending': true } ; +const signedTxp: ITxProposal = { 'actions': [{ 'version': '1.0.0', 'createdOn': 1588099987, 'copayerId': '671fee02a6c1c4de2e2609f9f9a6180dc03acfff6b759fe0b13a616ed4880065', 'type': 'accept', 'signatures': ['3045022100ace0efb22eaf21b77d5e4a31a491ad06fbcfe700ace5abe8453ec0328c714cba02205090a44975d4b65b5e2f4473da038e83875826b369fc79558a741268ed574bb0'], 'xpub': 'xpub6DVmxcjRgZdHNSEcXSiFtweVwMSTc3TMwRJ45nJYvyqvLbK1poPerupqh87rSoz27wvckb1CKnGZoLmLXSZyNGZtVd7neqSvdwJL6fceQpe', 'comment': null }], 'version': 3, 'createdOn': 1588099987, 'id': '7cea0d95-3308-48e7-a4be-3f16d66e1f5a', 'walletId': '7eaf4d32-c2fd-4262-864a-4c42fc9236f8', 'creatorId': '671fee02a6c1c4de2e2609f9f9a6180dc03acfff6b759fe0b13a616ed4880065', 'coin': 'bch', 'chain': 'bch', 'network': 'livenet', 'outputs': [{ 'amount': 80000000, 'toAddress': 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X' }], 'amount': 80000000, 'message': 'some message', 'payProUrl': null, 'changeAddress': { 'version': '1.0.0', 'createdOn': 1588099987, 'address': 'CWwtFMy3GMr5qMEtvEdUDjePfShzkJXCnh', 'walletId': '7eaf4d32-c2fd-4262-864a-4c42fc9236f8', 'isChange': true, 'path': 'm/1/0', 'publicKeys': ['02129acdcc600694b3ce55a2d05244186e806174eb0bafde20e5a6395ded647857'], 'coin': 'bch', 'network': 'livenet', 'type': 'P2PKH', 'hasActivity': null, 'beRegistered': null }, 'inputs': [{ 'txid': '4e7feb01fb039f3a719e9390c8f7cb501fb8a0a083f72a43b4cbb420d8505cc0', 'vout': 5, 'satoshis': 100000000, 'scriptPubKey': '76a914d391e62337ed194a1e428f32d14838e5d848180a88ac', 'address': 'qrfere3rxlk3jjs7g28n952g8rjasjqcpgx3axq70t', 'confirmations': 44, 'publicKeys': ['024d27ca79a3ed27a143cb9d1dff01e4e6445294679a700ca404ca449211d08aa7'], 'wallet': '7eaf4d32-c2fd-4262-864a-4c42fc9236f8', 'path': 'm/0/1' }], 'walletM': 1, 'walletN': 1, 'requiredSignatures': 1, 'requiredRejections': 1, 'status': 'accepted', 'txid': '8541ed35ac43e07e362a00ebab9448eefa63840c75ca38edff6785c223503a29', 'broadcastedOn': null, 'inputPaths': ['m/0/1'], 'outputOrder': [1, 0], 'fee': 2430, 'feeLevel': null, 'feePerKb': 10000, 'excludeUnconfirmedUtxos': false, 'addressType': 'P2PKH', 'customData': null, 'proposalSignature': '3045022100e8a1ac6eef882fb3e7311c725093317df99e5ed46f52fdee9064560bb757d1c902200b09f806a17d4c7950097b8714293725ce741c6de25914f81a8ca0ca1107fc4c', 'signingMethod': 'ecdsa', 'proposalSignaturePubKey': null, 'proposalSignaturePubKeySig': null, 'lockUntilBlockHeight': null, 'gasPrice': null, 'from': null, 'nonce': null, 'gasLimit': null, 'data': null, 'tokenAddress': null, 'destinationTag': null, 'invoiceID': null, 'creatorName': 'copayer 1', 'raw': '0100000001c05c50d820b4cbb4432af783a0a0b81f50cbf7c890939e713a9f03fb01eb7f4e050000006b483045022100ace0efb22eaf21b77d5e4a31a491ad06fbcfe700ace5abe8453ec0328c714cba02205090a44975d4b65b5e2f4473da038e83875826b369fc79558a741268ed574bb04121024d27ca79a3ed27a143cb9d1dff01e4e6445294679a700ca404ca449211d08aa7ffffffff0282233101000000001976a9149edd2399faccf4e57df08bef78962fa0228741cf88ac00b4c404000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac00000000' }; diff --git a/packages/bitcore-wallet-service/test/chain/doge.test.ts b/packages/bitcore-wallet-service/test/chain/doge.test.ts index 9986b40ab1c..a5e86ea9213 100644 --- a/packages/bitcore-wallet-service/test/chain/doge.test.ts +++ b/packages/bitcore-wallet-service/test/chain/doge.test.ts @@ -168,7 +168,7 @@ const aTXP = () => { coin: 'doge', chain: 'doge', network: 'livenet', - amount: (1.2550574e14).toString(), + amount: 1.2550574e14, message: 'some message', proposalSignature: '304402207e8ba2f9e88c1e7a76979f7e9df4d1cf43784d196b4b92f7f605386d0562216c022025356aae37397771a5fcb7520e97096fed197598a0e68883f6b30f3ccddd636c', changeAddress: { @@ -240,7 +240,7 @@ const aTXP = () => { return txp; }; -const signedTxp = { +const signedTxp: ITxProposal = { actions: [ { version: '1.0.0', @@ -261,6 +261,7 @@ const signedTxp = { walletId: 'd98853c7-5a4b-48de-9f6c-8fb36aa271f1', creatorId: '5a49148ea402f06943b511912f31bc9114153e6055c38642fe789f7b2c7bf9f8', coin: 'doge', + chain: 'doge', network: 'testnet', outputs: [ { @@ -339,9 +340,6 @@ const signedTxp = { multisigTxId: null, destinationTag: null, invoiceID: null, - derivationStrategy: 'BIP44', creatorName: 'copayer 1', - raw: '0100000001b8a1d4773add3d5ca38652537310e9bd64d812d771e16f62b4a6e33e79f85453000000006b483045022100a9bee101f13eeb8f6bdf1b6421974a17b2aeb0bcf080526df20e93483a67bbca022071ba69e707430136fb65488b1e5413eb54279770b74b7e15fe15aeac16770ceb012102f4526941f57f37b8f1cd4970091ebcd1701980a82d26a781b2e44b384609eb22ffffffff026c81c9f6160900001976a914aeb332ea003a7efb5dab0dcc56d19fed84e26afb88acf4cb5156010000001976a914afbf96bfb28815cad8205c8f2f5a86819136664c88ac00000000', - note: null, - isPending: false + raw: '0100000001b8a1d4773add3d5ca38652537310e9bd64d812d771e16f62b4a6e33e79f85453000000006b483045022100a9bee101f13eeb8f6bdf1b6421974a17b2aeb0bcf080526df20e93483a67bbca022071ba69e707430136fb65488b1e5413eb54279770b74b7e15fe15aeac16770ceb012102f4526941f57f37b8f1cd4970091ebcd1701980a82d26a781b2e44b384609eb22ffffffff026c81c9f6160900001976a914aeb332ea003a7efb5dab0dcc56d19fed84e26afb88acf4cb5156010000001976a914afbf96bfb28815cad8205c8f2f5a86819136664c88ac00000000' }; diff --git a/packages/bitcore-wallet-service/test/chain/ltc.test.ts b/packages/bitcore-wallet-service/test/chain/ltc.test.ts index 5cf7bb0fa2d..450a4951240 100644 --- a/packages/bitcore-wallet-service/test/chain/ltc.test.ts +++ b/packages/bitcore-wallet-service/test/chain/ltc.test.ts @@ -168,7 +168,7 @@ const aTXP = () => { coin: 'ltc', chain: 'ltc', network: 'livenet', - amount: '30000000', + amount: 30000000, message: 'some message', proposalSignature: '7035022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9', changeAddress: { @@ -233,7 +233,7 @@ const aTXP = () => { return txp; }; -const signedTxp = { +const signedTxp: ITxProposal = { actions: [ { version: '1.0.0', @@ -250,6 +250,7 @@ const signedTxp = { ], version: 3, createdOn: 1626385821, + creatorName: 'Ben', id: 'b89d4f4b-0c36-4054-8e09-0d3867f1fc89', walletId: '10f76944-ac95-494a-ba32-1eba9a581332', creatorId: '16f48373387600f00741e74488da2f82409e82105f515e6c7a856368cc95fc78', @@ -261,7 +262,6 @@ const signedTxp = { amount: 10000, toAddress: 'mu2QPdDVzsuAJAcMKbhqWqZYfeWcAonGEf', message: null, - encryptedMessage: null } ], amount: 10000, @@ -323,6 +323,5 @@ const signedTxp = { multisigContractAddress: undefined, multisigTxId: undefined, destinationTag: undefined, - invoiceID: undefined, - isPending: false + invoiceID: undefined }; diff --git a/packages/bitcore-wallet-service/test/chain/xrp.test.ts b/packages/bitcore-wallet-service/test/chain/xrp.test.ts index 8d9d731d994..b9e342df1d3 100644 --- a/packages/bitcore-wallet-service/test/chain/xrp.test.ts +++ b/packages/bitcore-wallet-service/test/chain/xrp.test.ts @@ -91,7 +91,7 @@ const aTXP = function() { excludeUnconfirmedUtxos: true, addressType: 'P2PKH', customData: null, - amount: '1000000', + amount: 1000000, fee: 10, version: 3, broadcastedOn: 1763669150, @@ -108,12 +108,11 @@ const aTXP = function() { return txp; }; -const signedTxp = { +const signedTxp: ITxProposal = { creatorName: '{"iv":"aN+YwTvJRK73M7FfCFzIzA==","v":1,"iter":1,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","ct":"TqFvhWq2/F6ykA=="}', createdOn: 1763669147, id: 'a911faec-975e-4c53-a852-8ebf579ff1f6', txid: 'B6BB3DE3F87395619D108DC35E2CC88440C998D854A12E2E312BC5A8DC11F121', - txids: '', walletId: '093352fc-a597-4837-94ee-8b9e3f1a5039', creatorId: 'c10c8e84ac963c539d86451aea8ce6f92dfaeeebbef1f5dd212d324301ea278f', coin: 'xrp', @@ -122,8 +121,6 @@ const signedTxp = { message: '', payProUrl: '', from: 'rPsaG3gUYCPdoN2X1EgynYfYbwKeSbdTCN', - changeAddress: '', - escrowAddress: '', inputs: [], outputs: [{ amount: 1000000, @@ -165,42 +162,10 @@ const signedTxp = { proposalSignaturePubKey: '', proposalSignaturePubKeySig: '', signingMethod: 'ecdsa', - lowFees: '', raw: [ '12000022800000002400BF209C6140000000000F424068400000000000000A7321024DDE2306BEACF6D450495F40E69400824C99235FECEB742952FBA550C91F9C597446304402206188D1A90328D034F7D01E0ECEB6629AAEF47AF09EF33C4BF91871076AE60C81022056EE1DB7B90B7A701087FA8135E434616C5824F1CCC3FC87F741771E22BE9A138114F1B7FDA196A474A7A5CE0588E03B1BE41F8605978314D51E9EDD6905A6D2FDB4ADD86744C56323439285' ], nonce: 12525724, - gasPrice: '', - maxGasFee: '', - priorityGasFee: '', - txType: '', - gasLimit: '', - data: '', - tokenAddress: '', - multisigContractAddress: '', - multisigTxId: '', - destinationTag: '', - invoiceID: '', - lockUntilBlockHeight: '', - instantAcceptanceEscrow: '', - isTokenSwap: '', - multiSendContractAddress: '', - enableRBF: '', - replaceTxByFee: '', - multiTx: '', - space: '', - nonceAddress: '', - blockHash: '', - blockHeight: '', - category: '', - priorityFee: '', - computeUnits: '', - memo: '', - fromAta: '', - decimals: '', - refreshOnPublish: '', - prePublishRaw: '', - note: '', - derivationStrategy: 'BIP44', - isPending: false + destinationTag: null, + invoiceID: '' }; diff --git a/packages/bitcore-wallet-service/test/model/txproposal.test.ts b/packages/bitcore-wallet-service/test/model/txproposal.test.ts index 4a3d1c29be1..f2291db040c 100644 --- a/packages/bitcore-wallet-service/test/model/txproposal.test.ts +++ b/packages/bitcore-wallet-service/test/model/txproposal.test.ts @@ -2,6 +2,7 @@ import * as chai from 'chai'; import 'chai/register-should'; +import { Utils as CWCUtils } from '@bitpay-labs/crypto-wallet-core'; import { TxProposal } from '../../src/lib/model/txproposal'; const should = chai.should(); @@ -101,6 +102,61 @@ describe('TxProposal', function() { }); }); + describe('#formatNumbers', function() { + it('should format numbers as strings', function() { + const t = TxProposal.fromObj(aTXP()); + t.amount.should.be.a('number'); + const txp = TxProposal.formatNumbers(t, 'string'); + txp.amount.should.be.a('string'); + txp.fee.should.be.a('string'); + txp.outputs[0].amount.should.be.a('string'); + }); + + it('should format numbers as hex', function() { + const t = TxProposal.fromObj(aTXP()); + const txp = TxProposal.formatNumbers(t, 'hex'); + txp.amount.should.be.a('string'); + CWCUtils.isHexString(txp.amount).should.equal(true); + txp.fee.should.be.a('string'); + CWCUtils.isHexString(txp.fee).should.equal(true); + txp.outputs[0].amount.should.be.a('string'); + CWCUtils.isHexString(txp.outputs[0].amount).should.equal(true); + }); + + it('should format numbers as bigints', function() { + const t = TxProposal.fromObj(aTXP()); + const txp = TxProposal.formatNumbers(t, 'bigint'); + txp.amount.should.be.a('bigint'); + txp.fee.should.be.a('bigint'); + txp.outputs[0].amount.should.be.a('bigint'); + }); + + it('should format numbers as numbers', function() { + const t1 = TxProposal.fromObj(aTXP()); + const t2 = TxProposal.formatNumbers(t1, 'string'); + t2.amount.should.be.a('string'); + + const txp = TxProposal.formatNumbers(t2, 'number'); + txp.amount.should.be.a('number'); + txp.fee.should.be.a('number'); + txp.outputs[0].amount.should.be.a('number'); + }); + + it('should format string numbers as hex', function() { + const t1 = TxProposal.fromObj(aTXP()); + const t2 = TxProposal.formatNumbers(t1, 'string'); + t2.amount.should.be.a('string'); + t2.amount.startsWith('0x').should.equal(false); + + const txp = TxProposal.formatNumbers(t2, 'hex'); + txp.amount.should.be.a('string'); + CWCUtils.isHexString(txp.amount).should.equal(true); + txp.fee.should.be.a('string'); + CWCUtils.isHexString(txp.fee).should.equal(true); + txp.outputs[0].amount.should.be.a('string'); + CWCUtils.isHexString(txp.outputs[0].amount).should.equal(true); + }); + }); }); const theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e'; @@ -142,6 +198,11 @@ const aTXP = function() { version: '1.0.0', createdOn: 1424372337, address: '3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH', + walletId: 'thisdoesntmatter', + coin: 'btc', + chain: 'btc', + network: 'livenet', + type: 'P2SH', path: 'm/2147483647/1/0', publicKeys: [ '030562cb099e6043dc499eb359dd97c9d500a3586498e4bcf0228a178cc20e6f16', @@ -166,15 +227,17 @@ const aTXP = function() { requiredRejections: 1, walletN: 2, addressType: 'P2SH', - status: 'pending', + status: 'pending' as const, actions: [], fee: 10000, outputs: [{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + address: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 10000000, message: 'first message' }, { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + address: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 20000000, message: 'second message' }], diff --git a/packages/crypto-rpc/lib/sol/SplRpc.js b/packages/crypto-rpc/lib/sol/SplRpc.js index 914e4409dff..aba6288ee8d 100644 --- a/packages/crypto-rpc/lib/sol/SplRpc.js +++ b/packages/crypto-rpc/lib/sol/SplRpc.js @@ -58,8 +58,8 @@ export class SplRpc extends SolRpc { authority: fromAccountKeypairSigner.address, mint: SolKit.address(mintAddress), destination: destinationAtaAddress, - amount, - decimals + amount: BigInt(amount), + decimals: Number(decimals) }) ], transactionMessage diff --git a/packages/crypto-wallet-core/src/transactions/spl/index.ts b/packages/crypto-wallet-core/src/transactions/spl/index.ts index 6f0311ab27a..afa0d6de210 100644 --- a/packages/crypto-wallet-core/src/transactions/spl/index.ts +++ b/packages/crypto-wallet-core/src/transactions/spl/index.ts @@ -28,7 +28,7 @@ export class SPLTxProvider extends SOLTxProvider { mint: SolKit.address(tokenAddress), destination: SolKit.address(recipientAddress), // ATA address amount: BigInt(recipientAmount), - decimals + decimals: Number(decimals) })); } } @@ -164,7 +164,7 @@ interface CreateParams { // SPL token transfer fields (required for token transfers) tokenAddress: string; // mint address fromAta?: string; - decimals?: number; + decimals?: number | string; } interface CreateRecoverNestedAssociatedTokenParams { diff --git a/packages/crypto-wallet-core/src/utils/index.ts b/packages/crypto-wallet-core/src/utils/index.ts index 207f3c2c480..18152f7fcfa 100644 --- a/packages/crypto-wallet-core/src/utils/index.ts +++ b/packages/crypto-wallet-core/src/utils/index.ts @@ -63,7 +63,10 @@ export function isHexString(str: string): boolean { if (typeof str !== 'string' || str === '') { return false; } - const normalizedStr = str.toLowerCase().slice(0, 2) === '0x' ? str.toLowerCase().slice(2) : str.toLowerCase(); + let normalizedStr = str.toLowerCase().slice(0, 2) === '0x' ? str.toLowerCase().slice(2) : str.toLowerCase(); + if (normalizedStr.length % 2) { + normalizedStr = '0' + normalizedStr; // add leading zero if odd length + } return Buffer.from(normalizedStr, 'hex').toString('hex') === normalizedStr; } diff --git a/packages/crypto-wallet-core/test/utils.test.ts b/packages/crypto-wallet-core/test/utils.test.ts index 8a53850eafb..a22d11a1397 100644 --- a/packages/crypto-wallet-core/test/utils.test.ts +++ b/packages/crypto-wallet-core/test/utils.test.ts @@ -15,6 +15,12 @@ describe('Utils', function() { expect(result).to.equal(true); }); + it('should return true for valid odd-length hex string', function() { + const str = 'abcdef12345'; + const result = utils.isHexString(str); + expect(result).to.be.true; + }); + it('should return false for invalid prefixed hex string', function() { const str = '0xabc123g'; const result = utils.isHexString(str);