From 5ff87d7724cabeccf683d1bd7c73eb2efd42f9c5 Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Tue, 2 Sep 2025 16:15:45 -0400 Subject: [PATCH 1/4] added utxo & xrp tx signing support --- packages/bitcore-cli/src/cli-commands.ts | 2 +- packages/bitcore-cli/src/cli.ts | 13 ++-- .../src/commands/create/createThresholdSig.ts | 2 +- .../bitcore-cli/src/commands/transaction.ts | 13 ++-- .../bitcore-cli/src/commands/txproposals.ts | 27 ++++++-- packages/bitcore-cli/src/constants.ts | 26 +++++++- packages/bitcore-cli/src/prompts.ts | 13 +++- packages/bitcore-cli/src/tss.ts | 10 +-- packages/bitcore-cli/src/utils.ts | 61 +++++++++++++++-- packages/bitcore-cli/src/wallet.ts | 65 +++++++++++++++---- packages/bitcore-cli/types/wallet.d.ts | 3 + packages/bitcore-lib/lib/crypto/signature.js | 2 +- packages/bitcore-wallet-client/src/index.ts | 4 +- .../src/lib/common/constants.ts | 1 + .../src/lib/chain/xrp/index.ts | 13 ++-- .../src/constants/chains.ts | 3 +- .../crypto-wallet-core/src/constants/index.ts | 4 +- .../src/transactions/btc/index.ts | 51 ++++++++++++++- .../src/transactions/eth/index.ts | 10 +++ .../src/transactions/index.ts | 8 +++ .../src/transactions/sol/index.ts | 5 ++ .../src/transactions/xrp/index.ts | 44 +++++++++---- 22 files changed, 304 insertions(+), 76 deletions(-) diff --git a/packages/bitcore-cli/src/cli-commands.ts b/packages/bitcore-cli/src/cli-commands.ts index 745674babe1..30342bcc4e7 100644 --- a/packages/bitcore-cli/src/cli-commands.ts +++ b/packages/bitcore-cli/src/cli-commands.ts @@ -14,7 +14,7 @@ export function getCommands(args: { wallet: IWallet, opts?: ICliOptions }) { { label: 'Import File', value: 'import-file', hint: 'Import using a file' }, ], BASIC: [ - { label: ({ token }) => `Token${token ? ` (${Utils.colorText(token, 'orange')})` : ''}`, value: 'token', hint: 'Manage the token context for this session', show: () => !wallet.isUtxo(), noCmd: true }, + { label: ({ token }) => `Token${token ? ` (${Utils.colorText(token, 'orange')})` : ''}`, value: 'token', hint: 'Manage the token context for this session', show: () => wallet.isTokenChain(), noCmd: true }, { label: ({ ppNum }) => `Proposals${ppNum}`, value: 'txproposals', hint: 'Get pending transaction proposals' }, { label: 'Send', value: 'transaction', hint: 'Create a transaction to send funds' }, { label: 'Receive', value: 'address', hint: 'Get an address to receive funds to' }, diff --git a/packages/bitcore-cli/src/cli.ts b/packages/bitcore-cli/src/cli.ts index 100ee04ca1b..aa79d0298fa 100755 --- a/packages/bitcore-cli/src/cli.ts +++ b/packages/bitcore-cli/src/cli.ts @@ -111,7 +111,8 @@ if (require.main === module) { if (walletName === 'list') { for (const file of fs.readdirSync(opts.dir)) { if (file.endsWith('.json')) { - console.log(`- ${file.replace('.json', '')}`); + const walletData = JSON.parse(fs.readFileSync(path.join(opts.dir, file), 'utf8')); + console.log(` ${Utils.boldText(file.replace('.json', ''))} [${Utils.colorizeChain(walletData.creds.chain)}:${walletData.creds.network}]`); } } return; @@ -125,7 +126,7 @@ if (require.main === module) { }; if (!wallet.client?.credentials) { - prompt.intro(`No wallet found named ${Utils.colorText(walletName, 'orange')}`); + prompt.intro(`No wallet found named ${Utils.underlineText(Utils.boldText(Utils.italicText(walletName)))}`); const action: NewCommand | symbol = await prompt.select({ message: 'What would you like to do?', options: [].concat(COMMANDS.NEW, COMMANDS.EXIT) @@ -156,20 +157,20 @@ if (require.main === module) { opts.exit = true; break; } - prompt.outro(`${Utils.colorText('✔', 'green')} Wallet ${Utils.colorText(walletName, 'orange')} created successfully!`); + !opts.exit && prompt.outro(`${Utils.colorText('✔', 'green')} Wallet ${Utils.boldText(walletName)} created successfully!`); } else { if (opts.status) { - prompt.intro(`Status for ${Utils.colorText(walletName, 'orange')}`); + prompt.intro(`Status for ${Utils.colorTextByChain(wallet.chain, walletName)}`); const status = await commands.status.walletStatus({ wallet, opts }); cmdParams.status = status; - prompt.outro('Welcome to the Bitcore CLI!'); + prompt.outro(Utils.boldText('Welcome to the Bitcore CLI!')); } let advancedActions = false; do { // Don't display the intro if running a specific command - !opts.command && prompt.intro(`${Utils.colorText('~~ Main Menu ~~', 'blue')} (${Utils.colorText(walletName, 'orange')})`); + !opts.command && prompt.intro(`${Utils.boldText('[ Main Menu')} - ${Utils.colorTextByChain(wallet.chain, walletName)} ${Utils.boldText(']')}`); cmdParams.status.pendingTxps = opts.command ? [] : await wallet.client.getTxProposals({}); const dynamicCmdArgs = { diff --git a/packages/bitcore-cli/src/commands/create/createThresholdSig.ts b/packages/bitcore-cli/src/commands/create/createThresholdSig.ts index 2f7306be49c..fc167c23374 100644 --- a/packages/bitcore-cli/src/commands/create/createThresholdSig.ts +++ b/packages/bitcore-cli/src/commands/create/createThresholdSig.ts @@ -21,7 +21,7 @@ export async function createThresholdSigWallet( const { verbose, mnemonic } = opts; const copayerName = await getCopayerName(); - const addressType = await getAddressType({ chain, network, isMultiSig: false }); // TSS is treated as a single-sig + const addressType = await getAddressType({ chain, network, isMultiSig: false, isTss: true }); const password = await getPassword('Enter a password for the wallet:', { hidden: false }); let key; diff --git a/packages/bitcore-cli/src/commands/transaction.ts b/packages/bitcore-cli/src/commands/transaction.ts index e4de463f5ff..7b6916fe9d7 100755 --- a/packages/bitcore-cli/src/commands/transaction.ts +++ b/packages/bitcore-cli/src/commands/transaction.ts @@ -1,5 +1,5 @@ import * as prompt from '@clack/prompts'; -import { type Txp } from 'bitcore-wallet-client'; +import { type Txp, Utils as BWCUtils } from 'bitcore-wallet-client'; import { Validation } from 'crypto-wallet-core'; import os from 'os'; import type { CommonArgs } from '../../types/cli'; @@ -73,13 +73,14 @@ export async function createTransaction( throw new Error(`Unknown token "${opts.tokenAddress || opts.token}" on ${chain}:${network}`); } } + const nativeCurrency = (await wallet.getNativeCurrency(true)).displayCode; if (!status) { status = await wallet.client.getStatus({ tokenAddress: tokenObj?.contractAddress }); } const { balance } = status; - const currency = tokenObj?.displayCode || chain.toUpperCase(); + const currency = tokenObj?.displayCode || nativeCurrency; const availableAmount = Utils.amountFromSats(chain, balance.availableAmount, tokenObj); if (!balance.availableAmount) { @@ -89,8 +90,7 @@ export async function createTransaction( const to = opts.to || await prompt.text({ - message: 'Enter the recipient address:', - placeholder: 'e.g. n2HRFgtoihgAhx1qAEXcdBMjoMvAx7AcDc', + message: 'Enter the recipient\'s address:', validate: (value) => { if (!Validation.validateAddress(chain, network, value)) { return `Invalid address for ${chain}:${network}`; @@ -121,7 +121,7 @@ export async function createTransaction( if (isNaN(val) || val <= 0) { return 'Please enter a valid amount greater than 0'; } - if (val > availableAmount) { + if (val > Number(availableAmount)) { return 'You cannot send more than your balance'; } return; // valid value @@ -179,6 +179,9 @@ export async function createTransaction( if (prompt.isCancel(customFeeRate)) { throw new UserCancelled(); } + if (BWCUtils.isUtxoChain(chain)) { + customFeeRate = (Number(customFeeRate) * 1000).toString(); // convert to sats/KB + } } const txpParams = { diff --git a/packages/bitcore-cli/src/commands/txproposals.ts b/packages/bitcore-cli/src/commands/txproposals.ts index bc95ac8e682..4eccb0d3fe0 100755 --- a/packages/bitcore-cli/src/commands/txproposals.ts +++ b/packages/bitcore-cli/src/commands/txproposals.ts @@ -5,6 +5,7 @@ import type { CommonArgs } from '../../types/cli'; import { UserCancelled } from '../errors'; import { getAction, getFileName } from '../prompts'; import { Utils } from '../utils'; +import { ITokenObj } from 'types/wallet'; export function command(args: CommonArgs) { const { wallet, program } = args; @@ -64,15 +65,27 @@ export async function getTxProposals( } else { const lines = []; const chain = txp.chain || txp.coin; - const currency = chain.toUpperCase(); - const feeCurrency = currency; // TODO + const network = txp.network; + let tokenObj: ITokenObj; + if (txp.tokenAddress) { + tokenObj = await wallet.getToken({ tokenAddress: txp.tokenAddress }); + if (!tokenObj) { + throw new Error(`Unknown token "${txp.tokenAddress}" on ${chain}:${network}`); + } + } + const nativeCurrency = (await wallet.getNativeCurrency(true)).displayCode; + const currency = tokenObj?.displayCode || nativeCurrency; lines.push(`Chain: ${chain.toUpperCase()}`); lines.push(`Network: ${Utils.capitalize(txp.network)}`); txp.tokenAddress && lines.push(`Token: ${txp.tokenAddress}`); - lines.push(`Amount: ${Utils.amountFromSats(chain, txp.amount)} ${currency}`); - lines.push(`Fee: ${Utils.amountFromSats(chain, txp.fee)} ${feeCurrency}`); - lines.push(`Total Amount: ${Utils.amountFromSats(chain, txp.amount + txp.fee)} ${currency}`); + lines.push(`Amount: ${Utils.renderAmount(currency, txp.amount, tokenObj)}`); + lines.push(`Fee: ${Utils.renderAmount(nativeCurrency, txp.fee)}`); + // 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) + }`); txp.gasPrice && lines.push(`Gas Price: ${Utils.displayFeeRate(chain, txp.gasPrice)}`); txp.gasLimit && lines.push(`Gas Limit: ${txp.gasLimit}`); txp.feePerKb && lines.push(`Fee Rate: ${Utils.displayFeeRate(chain, txp.feePerKb)}`); @@ -85,9 +98,9 @@ export async function getTxProposals( lines.push('---------------------------'); lines.push('Recipients:'); lines.push(...txp.outputs.map(o => { - return ` → ${Utils.maxLength(o.toAddress)}${o.tag ? `:${o.tag}` : ''}: ${Utils.amountFromSats(chain, o.amount)} ${currency}${o.message ? ` (${o.message})` : ''}`; + return ` → ${Utils.maxLength(o.toAddress)}${o.tag ? `:${o.tag}` : ''}: ${Utils.renderAmount(currency, o.amount)}${o.message ? ` (${o.message})` : ''}`; })); - txp.changeAddress && lines.push(`Change Address: ${Utils.maxLength(txp.changeAddress.address)} (${txp.changeAddress.path})`); + txp.changeAddress && lines.push(` ↲ ${Utils.maxLength(txp.changeAddress.address)} (change - ${txp.changeAddress.path})`); lines.push('---------------------------'); if (txp.actions?.length) { lines.push('Actions:'); diff --git a/packages/bitcore-cli/src/constants.ts b/packages/bitcore-cli/src/constants.ts index e7e139fd68b..612e1cc6f40 100644 --- a/packages/bitcore-cli/src/constants.ts +++ b/packages/bitcore-cli/src/constants.ts @@ -113,6 +113,13 @@ export const Constants = { yellow: '\x1b[33m%s\x1b[0m', blue: '\x1b[34m%s\x1b[0m', orange: '\x1b[38;5;208m%s\x1b[0m', + gold: '\x1b[38;5;214m%s\x1b[0m', + tan: '\x1b[38;5;180m%s\x1b[0m', + beige: '\x1b[38;5;223m%s\x1b[0m', + purple: '\x1b[38;5;129m%s\x1b[0m', + lightgray: '\x1b[38;5;250m%s\x1b[0m', + darkgray: '\x1b[38;5;236m%s\x1b[0m', + pink: '\x1b[38;5;213m%s\x1b[0m', none: '\x1b[0m%s', }, ADDRESS_TYPE: { @@ -125,6 +132,11 @@ export const Constants = { multiSig: { P2WSH: 'witnessscripthash', P2SH: 'scripthash', + }, + thresholdSig: { + P2WSH: 'witnessscripthash', + P2PKH: 'pubkeyhash', + // TSS doesn't support schnorr sigs, hence no P2TR } }, BCH: { @@ -133,6 +145,9 @@ export const Constants = { }, multiSig: { P2SH: 'scripthash' + }, + thresholdSig: { + P2PKH: 'pubkeyhash', } }, LTC: { @@ -143,7 +158,11 @@ export const Constants = { multiSig: { P2WSH: 'witnessscripthash', P2SH: 'scripthash', - } + }, + thresholdSig: { + P2WPKH: 'witnesspubkeyhash', + P2PKH: 'pubkeyhash', + } }, DOGE: { singleSig: { @@ -151,9 +170,12 @@ export const Constants = { }, multiSig: { P2SH: 'scripthash' + }, + thresholdSig: { + P2PKH: 'pubkeyhash', } }, - default: 'scripthash' + default: 'pubkeyhash' } }; diff --git a/packages/bitcore-cli/src/prompts.ts b/packages/bitcore-cli/src/prompts.ts index 8ab69438eb5..dec6f973629 100644 --- a/packages/bitcore-cli/src/prompts.ts +++ b/packages/bitcore-cli/src/prompts.ts @@ -1,6 +1,6 @@ import * as prompt from '@clack/prompts'; import { Network } from 'bitcore-wallet-client'; -import { BitcoreLib, BitcoreLibLtc } from 'crypto-wallet-core'; +import { BitcoreLib, BitcoreLibLtc, Constants as CWCConst } from 'crypto-wallet-core'; import { Constants } from './constants'; import { UserCancelled } from './errors'; import { Utils } from './utils'; @@ -17,6 +17,12 @@ export async function getChain(): Promise { message: 'Chain:', placeholder: `Default: ${defaultVal}`, defaultValue: defaultVal, + validate: (input) => { + if (CWCConst.CHAINS.includes(input?.toLowerCase())) { + return; // valid input + } + return `Invalid chain '${input}'. Valid options are: ${CWCConst.CHAINS.join(', ')}`; + } }); if (prompt.isCancel(chain)) { throw new UserCancelled(); @@ -147,7 +153,8 @@ export async function getCopayerName() { return copayerName as string; }; -export async function getAddressType({ chain, network, isMultiSig }: { chain: string; network?: Network; isMultiSig?: boolean }) { +export async function getAddressType(args: { chain: string; network?: Network; isMultiSig?: boolean; isTss?: boolean; }) { + const { chain, network, isMultiSig, isTss } = args; let addressTypes = Constants.ADDRESS_TYPE[chain.toUpperCase()]; if (!addressTypes) { return Constants.ADDRESS_TYPE.default; @@ -155,6 +162,8 @@ export async function getAddressType({ chain, network, isMultiSig }: { chain: st if (isMultiSig) { addressTypes = addressTypes.multiSig; + } else if (isTss) { + addressTypes = addressTypes.thresholdSig; } else { addressTypes = addressTypes.singleSig; } diff --git a/packages/bitcore-cli/src/tss.ts b/packages/bitcore-cli/src/tss.ts index 6ab8d1414ec..053795c0628 100644 --- a/packages/bitcore-cli/src/tss.ts +++ b/packages/bitcore-cli/src/tss.ts @@ -1,6 +1,6 @@ import * as prompt from '@clack/prompts'; -import { TssSign, Utils as BWCUtils } from 'bitcore-wallet-client'; -import { ethers, type Types as CWCTypes } from 'crypto-wallet-core'; +import { TssSign } from 'bitcore-wallet-client'; +import { type Types as CWCTypes, Transactions } from 'crypto-wallet-core'; import url from 'url'; import { type TssKeyType, @@ -25,12 +25,8 @@ export async function sign(args: { }): Promise> { const { host, chain, walletData, messageHash, derivationPath, password, id, logMessageWaiting, logMessageCompleted } = args; - const isEvm = BWCUtils.isEvmChain(chain); - const transformISignature = (signature: TssSign.ISignature): string => { - if (isEvm) { - return ethers.Signature.from(signature).serialized; - } + return Transactions.transformSignatureObject({ chain, obj: signature }); }; const tssSign = new TssSign.TssSign({ diff --git a/packages/bitcore-cli/src/utils.ts b/packages/bitcore-cli/src/utils.ts index 347da0c3e69..4b016bcf93f 100644 --- a/packages/bitcore-cli/src/utils.ts +++ b/packages/bitcore-cli/src/utils.ts @@ -52,6 +52,22 @@ export class Utils { return Constants.COLOR[color.toLowerCase()].replace('%s', text); } + static boldText(text: string) { + return '\x1b[1m' + text + '\x1b[0m'; + } + + static italicText(text: string) { + return '\x1b[3m' + text + '\x1b[0m'; + } + + static underlineText(text: string) { + return '\x1b[4m' + text + '\x1b[0m'; + } + + static strikeText(text: string) { + return '\x1b[9m' + text + '\x1b[0m'; + } + static capitalize(text: string): string { return text.charAt(0).toUpperCase() + text.slice(1); } @@ -95,8 +111,8 @@ export class Utils { return amountSat; }; - static renderAmount(currency: string, satoshis: number | bigint, opts = {}): string { - return BWCUtils.formatAmount(satoshis, currency.toLowerCase(), { ...opts, fullPrecision: true }) + ' ' + currency.toUpperCase(); + static renderAmount(currency: string, satoshis: number | bigint | string, opts?: ITokenObj): string { + return Utils.amountFromSats(currency, Number(satoshis), opts) + ' ' + currency.toUpperCase(); } static renderStatus(status: string): string { @@ -249,7 +265,7 @@ export class Utils { case 'drops': case 'lamports': default: - `${feeRate} ${feeUnit}`; + return `${feeRate} ${feeUnit}`; } } @@ -269,12 +285,12 @@ export class Utils { case 'doge': case 'ltc': case 'xrp': - return sats / 1e8; + return (sats / 1e8).toLocaleString('fullwide', { useGrouping: false, minimumFractionDigits: 0, maximumFractionDigits: 8 }); case 'sol': - return sats / 1e9; + return (sats / 1e9).toLocaleString('fullwide', { useGrouping: false, minimumFractionDigits: 0, maximumFractionDigits: 9 }); default: // Assume EVM chain - return sats / 1e18; + return (sats / 1e18).toLocaleString('fullwide', { useGrouping: false, minimumFractionDigits: 0, maximumFractionDigits: 18 }); } } @@ -371,4 +387,37 @@ export class Utils { } return fileName; } + + static getChainColor(chain: string) { + switch (chain.toLowerCase()) { + case 'btc': + return 'orange'; + case 'bch': + return 'green'; + case 'doge': + return 'beige'; + case 'ltc': + return 'lightgray'; + case 'eth': + return 'blue'; + case 'matic': + return 'pink'; + case 'xrp': + return 'darkgray'; + case 'sol': + return 'purple'; + } + } + + static colorTextByChain(chain: string, text: string) { + const color = Utils.getChainColor(chain); + if (!color) { + return Utils.boldText(text); + } + return Utils.colorText(text, color); + } + + static colorizeChain(chain: string) { + return Utils.colorTextByChain(chain, chain); + } }; diff --git a/packages/bitcore-cli/src/wallet.ts b/packages/bitcore-cli/src/wallet.ts index 72e6fb6de19..bd653c43ad9 100644 --- a/packages/bitcore-cli/src/wallet.ts +++ b/packages/bitcore-cli/src/wallet.ts @@ -11,15 +11,17 @@ import { Utils as BWCUtils } from 'bitcore-wallet-client'; import { - ethers, Message, type Types as CWCTypes, Utils as CWCUtils, - Web3 + Web3, + BitcoreLib, + Transactions } from 'crypto-wallet-core'; import fs from 'fs'; import path from 'path'; import url from 'url'; +import crypto from 'crypto'; import type { ClientType, ITokenObj, @@ -340,9 +342,14 @@ 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'] }; - const response = await fetch(urls[network], { method: 'GET', headers: { 'Content-Type': 'application/json' } }); - if (!response.ok) { - throw new Error(`Failed to fetch currencies for wallet token check: ${response.statusText}`); + let response: Response; + try { + response = await fetch(urls[network], { method: 'GET', headers: { 'Content-Type': 'application/json' } }); + if (!response.ok) { + throw new Error(`Failed to fetch currencies for wallet token check: ${response.statusText}`); + } + } catch (err) { + throw new Error(`Unable to fetch currencies from ${urls[network]}. ${err}`); } const { data: bpCurrencies } = await response.json(); Wallet._bpCurrencies = bpCurrencies.map(c => ({ @@ -412,6 +419,21 @@ export class Wallet implements IWallet { } as ITokenObj; } + async getNativeCurrency(fallback?: boolean): Promise { + const chain = this.chain.toUpperCase(); + let retval = null; + try { + const currencies = await Wallet.getCurrencies(this.network); + retval = currencies.find(c => c.chain === chain && c.native); + } catch (err) { + prompt.log.warn(`Unable to fetch native currency for ${chain}: ${err}`); + } + if (fallback && !retval) { + retval = { displayCode: chain }; + } + return retval; + } + async getPasswordWithRetry(): Promise { let password; if (this.isWalletEncrypted()) { @@ -456,20 +478,31 @@ export class Wallet implements IWallet { const isUtxo = BWCUtils.isUtxoChain(txp.chain); const isEvm = BWCUtils.isEvmChain(txp.chain); const isSvm = BWCUtils.isSvmChain(txp.chain); + const isXrp = BWCUtils.isXrpChain(txp.chain); - if (!isEvm) { - throw new Error('TSS signing is only supported for EVM chains at the moment.'); + if (isSvm) { + throw new Error('TSS wallets do not yet support Solana.'); } const sigs: string[] = []; - const inputPaths = !isUtxo && !Array.isArray(txp.inputPaths) ? ['m/0/0'] : txp.inputPaths; - for (const i in inputPaths) { - const derivationPath = inputPaths[i]; + const inputPaths = !isUtxo && !txp.inputPaths?.length ? ['m/0/0'] : txp.inputPaths; + const tx = BWCUtils.buildTx(txp); + const xPubKey = new BitcoreLib.HDPublicKey(this.#walletData.creds.clientDerivedPublicKey); - const messageHash = isEvm - ? ethers.keccak256(Client.getRawTx(txp)[0]).slice(2) // remove 0x prefix - : 'TODO'; + for (let i = 0; i < inputPaths.length; i++) { + const derivationPath = inputPaths[i]; + const pubKey = xPubKey.deriveChild(derivationPath).publicKey.toString(); + const txHex = tx.uncheckedSerialize(); + + const messageHash = Transactions.getSighash({ + chain: this.chain, + network: this.network, + tx: Array.isArray(txHex) ? txHex[0] : txHex, + index: i, + utxos: txp.inputs, + pubKey + }); const signature = await tssSign({ host: this.host, @@ -478,7 +511,7 @@ export class Wallet implements IWallet { messageHash: Buffer.from(messageHash, 'hex'), derivationPath, password, - id: `${txp.id}:${derivationPath}`, + id: `${txp.id}:${derivationPath.replace(/\//g, '-')}`, logMessageWaiting: `Signing tx input ${i} (${i + 1}/${inputPaths.length}). Waiting for all parties to join...`, logMessageCompleted: `Tx input ${i} complete (${i + 1}/${inputPaths.length})` }); @@ -606,4 +639,8 @@ export class Wallet implements IWallet { isXrp() { return BWCUtils.isXrpChain(this.chain); } + + isTokenChain() { + return this.isEvm() || this.isSvm(); + } }; \ No newline at end of file diff --git a/packages/bitcore-cli/types/wallet.d.ts b/packages/bitcore-cli/types/wallet.d.ts index 36dc605f85c..167e8167626 100644 --- a/packages/bitcore-cli/types/wallet.d.ts +++ b/packages/bitcore-cli/types/wallet.d.ts @@ -72,6 +72,7 @@ export interface IWallet { getTokenByAddress(args: { tokenAddress: string }): Promise; getTokenByName(args: { token: string }): Promise; getTokenFromChain(args: { address: string }): Promise; + getNativeCurrency(fallback?: boolean): Promise; getPasswordWithRetry(): Promise; signTxp(args: { txp: Txp }): Promise>; signAndBroadcastTxp(args: { txp: Txp; }): Promise; @@ -90,6 +91,7 @@ export interface IWallet { isEvm(): boolean; isSvm(): boolean; isXrp(): boolean; + isTokenChain(): boolean; } export interface ITokenObj { @@ -111,4 +113,5 @@ export interface ITokenObj { sanctioned?: boolean; symbol: string; trancheDecimals: number; + native: boolean; } \ No newline at end of file diff --git a/packages/bitcore-lib/lib/crypto/signature.js b/packages/bitcore-lib/lib/crypto/signature.js index 69d6847953f..3b3e0b6d01a 100644 --- a/packages/bitcore-lib/lib/crypto/signature.js +++ b/packages/bitcore-lib/lib/crypto/signature.js @@ -181,7 +181,7 @@ Signature.prototype.toCompact = function(i, compressed) { * Returns either a DER encoded buffer or a Schnorr encoded buffer if isSchnor == true */ Signature.prototype.toBuffer = Signature.prototype.toDER = function() { - if(this.isSchnorr) { + if (this.isSchnorr) { const hashTypeBuf = !this.nhashtype || this.nhashtype === Signature.SIGHASH_DEFAULT ? Buffer.alloc(0) : Buffer.from([this.nhashtype]); return Buffer.concat([this.r.toBuffer({ size: 32 }), this.s.toBuffer({ size: 32 }), hashTypeBuf]); } diff --git a/packages/bitcore-wallet-client/src/index.ts b/packages/bitcore-wallet-client/src/index.ts index c12d6fe46a0..7029e2fc0a8 100644 --- a/packages/bitcore-wallet-client/src/index.ts +++ b/packages/bitcore-wallet-client/src/index.ts @@ -15,9 +15,9 @@ export { PayPro } from './lib/paypro'; export { Key } from './lib/key'; export { Verifier } from './lib/verifier'; export { Encryption } from './lib/common/encryption'; -export * as EncryptionTypes from './lib/common/encryption'; +export type * as EncryptionTypes from './lib/common/encryption'; export { Utils } from './lib/common/utils'; -export * as UtilsTypes from './lib/common/utils'; +export type * as UtilsTypes from './lib/common/utils'; export { Errors } from './lib/errors'; export * as TssKey from './lib/tsskey'; diff --git a/packages/bitcore-wallet-client/src/lib/common/constants.ts b/packages/bitcore-wallet-client/src/lib/common/constants.ts index 917348ab57d..a3802b2d641 100644 --- a/packages/bitcore-wallet-client/src/lib/common/constants.ts +++ b/packages/bitcore-wallet-client/src/lib/common/constants.ts @@ -51,6 +51,7 @@ export const Constants = { UTXO_CHAINS: CWC.Constants.UTXO_CHAINS, EVM_CHAINS: CWC.Constants.EVM_CHAINS, SVM_CHAINS: CWC.Constants.SVM_CHAINS, + RIPPLE_CHAINS: CWC.Constants.RIPPLE_CHAINS, MULTISIG_CHAINS: CWC.Constants.MULTISIG_CHAINS, ETH_TOKEN_OPTS: CWC.Constants.ETH_TOKEN_OPTS, MATIC_TOKEN_OPTS: CWC.Constants.MATIC_TOKEN_OPTS, 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 febcbab5e45..d0e0a033bac 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts @@ -1,4 +1,4 @@ -import { Transactions, Validation } from 'crypto-wallet-core'; +import { Transactions, Validation, BitcoreLib, Deriver } from 'crypto-wallet-core'; import _ from 'lodash'; import { IWallet } from 'src/lib/model'; import { IAddress } from 'src/lib/model/address'; @@ -226,7 +226,7 @@ export class XrpChain implements IChain { checkUtxos(opts) { } checkValidTxAmount(output): boolean { - if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount < 0) { + if (!output.amount || isNaN(output.amount) || output.amount < 0) { return false; } return true; @@ -258,15 +258,20 @@ export class XrpChain implements IChain { } const chain = 'XRP'; // TODO use lowercase always to avoid confusion - const network = tx.network; + const network = tx.network || tx.toObject().network; const unsignedTxs = tx.uncheckedSerialize(); const signedTxs = []; const txids = []; for (let index = 0; index < signatures.length; index++) { + const pubKey = new BitcoreLib.HDPublicKey(xpub).deriveChild(inputPaths[index] || 'm/0/0').publicKey.toString(); + if (Deriver.getAddress(chain, network, pubKey) !== tx.toObject().from) { // sanity check + throw new Error('Unknown public key for signature'); + } const signed = Transactions.applySignature({ chain, tx: unsignedTxs[index], - signature: signatures[index] + signature: signatures[index], + pubKey }); signedTxs.push(signed); diff --git a/packages/crypto-wallet-core/src/constants/chains.ts b/packages/crypto-wallet-core/src/constants/chains.ts index 784c47a8b27..450009bd3b2 100644 --- a/packages/crypto-wallet-core/src/constants/chains.ts +++ b/packages/crypto-wallet-core/src/constants/chains.ts @@ -2,7 +2,8 @@ export const UTXO_CHAINS = ['btc', 'bch', 'doge', 'ltc']; export const EVM_CHAINS = ['eth', 'matic', 'arb', 'base', 'op']; export const SVM_CHAINS = ['sol']; -export const CHAINS = [...UTXO_CHAINS, ...EVM_CHAINS, ...SVM_CHAINS]; +export const RIPPLE_CHAINS = ['xrp']; +export const CHAINS = [...UTXO_CHAINS, ...EVM_CHAINS, ...SVM_CHAINS, ...RIPPLE_CHAINS]; export const MULTISIG_CHAINS = UTXO_CHAINS; diff --git a/packages/crypto-wallet-core/src/constants/index.ts b/packages/crypto-wallet-core/src/constants/index.ts index 19e252c8776..5680e2c5883 100644 --- a/packages/crypto-wallet-core/src/constants/index.ts +++ b/packages/crypto-wallet-core/src/constants/index.ts @@ -7,7 +7,8 @@ import { EVM_CHAINS, MULTISIG_CHAINS, SVM_CHAINS, - UTXO_CHAINS + UTXO_CHAINS, + RIPPLE_CHAINS, } from './chains'; import { FEE_MINIMUMS } from './feeMinimums'; import { TOKEN_OPTS as opts } from './tokens'; @@ -17,6 +18,7 @@ export const Constants = { UTXO_CHAINS, EVM_CHAINS, SVM_CHAINS, + RIPPLE_CHAINS, SCRIPT_TYPES, CHAINS, MULTISIG_CHAINS, diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index 33c6643251e..f016186fe96 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -2,6 +2,8 @@ import assert from 'assert'; import BitcoreLib from 'bitcore-lib'; import type { Key } from '../../types/derivation'; +const $ = BitcoreLib.util.preconditions; + interface TssSig { r: string; s: string; @@ -77,7 +79,8 @@ export class BTCTxProvider { throw new Error('function getSignature not implemented for UTXO coins'); } - _transformSignatureObject(obj, sigtype) { + transformSignatureObject(params: { obj: any; sigtype?: number }) { + const { obj, sigtype } = params; let { r, s, v, i, nhashtype } = obj; if (typeof r === 'string') { r = Buffer.from(r.startsWith('0x') ? r.slice(2) : r, 'hex'); @@ -100,7 +103,7 @@ export class BTCTxProvider { i = parseInt(i) || parseInt(v); nhashtype = sigtype ?? nhashtype; - return new this.lib.crypto.Signature({ r, s, i, nhashtype }); + return new this.lib.crypto.Signature({ r, s, i, nhashtype }).toString(); } applySignature(params: { tx: BitcoreLib.Transaction; signature: SignatureType; index: number; sigtype?: number; }) { @@ -116,7 +119,7 @@ export class BTCTxProvider { inputIndex: index, outputIndex: tx.inputs[index].outputIndex, prevTxId: tx.inputs[index].prevTxId, - signature: this._transformSignatureObject(signature, nhashtype), + signature: this.transformSignatureObject({ obj: signature, sigtype: nhashtype }), sigtype: nhashtype, }); } @@ -176,6 +179,48 @@ export class BTCTxProvider { }); return applicableUtxos.map(utxo => utxo.address); } + + getSighash(params: { + tx: string | BitcoreLib.Transaction; + index: number; + utxos?: BitcoreLib.Transaction.UnspentOutput[]; + pubKey?: string | BitcoreLib.PublicKey | BitcoreLib.HDPublicKey; + path?: string; + sigtype?: number; + // Multisig params for `associateInputs()` + /** Multisig public keys */ + pubKeys?: string[] | BitcoreLib.PublicKey[]; + /** Threshold for multisig */ + threshold?: number; + /** Options for multisig */ + opts?: any; + // end Multisig params for `associateInputs()` + }): string { + const { index, utxos, path, sigtype, pubKeys, threshold, opts } = params; + let { tx, pubKey } = params; + + if (!(tx instanceof this.lib.Transaction)) { + tx = new this.lib.Transaction(tx); + } + if (utxos) { + tx.associateInputs(utxos.map(this.lib.Transaction.UnspentOutput), pubKeys, threshold, opts); + } + $.checkState(tx.inputs[index].output instanceof this.lib.Transaction.Output, 'Input must have all utxo info'); + + pubKey = pubKey?.toString(); + if (pubKey) { + try { + pubKey = new this.lib.PublicKey(pubKey); + } catch { + $.checkArgument(path, '`path` param is required to derive child key'); + pubKey = new this.lib.HDPublicKey(pubKey).deriveChild(path).publicKey; + } + } + // Not all input types require the public key + $.checkState(!pubKey || pubKey instanceof this.lib.PublicKey, 'Invalid public key'); + + return tx.inputs[index].getSighash(tx, pubKey, index, sigtype).toString('hex'); + } } type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; \ No newline at end of file diff --git a/packages/crypto-wallet-core/src/transactions/eth/index.ts b/packages/crypto-wallet-core/src/transactions/eth/index.ts index bd2fc4619f0..c799061a446 100644 --- a/packages/crypto-wallet-core/src/transactions/eth/index.ts +++ b/packages/crypto-wallet-core/src/transactions/eth/index.ts @@ -166,4 +166,14 @@ export class ETHTxProvider { const signature = this.getSignatureObject({ tx, key }); return this.applySignature({ tx, signature }); } + + transformSignatureObject(params: { obj: any; }) { + const { obj } = params; + return ethers.Signature.from(obj).serialized; + } + + getSighash(params: { tx: string; }): string { + const { tx } = params; + return ethers.keccak256(tx).slice(2); // remove 0x prefix + } } diff --git a/packages/crypto-wallet-core/src/transactions/index.ts b/packages/crypto-wallet-core/src/transactions/index.ts index f31af473232..976ed5766ed 100644 --- a/packages/crypto-wallet-core/src/transactions/index.ts +++ b/packages/crypto-wallet-core/src/transactions/index.ts @@ -70,6 +70,14 @@ export class TransactionsProxy { getHash(params) { return this.get(params).getHash(params); } + + transformSignatureObject(params) { + return this.get(params).transformSignatureObject(params); + } + + getSighash(params): string { + return this.get(params).getSighash(params); + } } export default new TransactionsProxy(); diff --git a/packages/crypto-wallet-core/src/transactions/sol/index.ts b/packages/crypto-wallet-core/src/transactions/sol/index.ts index 89ae2aba11d..0e10fe50904 100644 --- a/packages/crypto-wallet-core/src/transactions/sol/index.ts +++ b/packages/crypto-wallet-core/src/transactions/sol/index.ts @@ -285,4 +285,9 @@ export class SOLTxProvider { return SolKit.getBase58Decoder().decode(signature); } + + getSighash(params: { tx: string; }): string { + const { tx } = params; + return null; // TODO + } } \ No newline at end of file diff --git a/packages/crypto-wallet-core/src/transactions/xrp/index.ts b/packages/crypto-wallet-core/src/transactions/xrp/index.ts index 43f5d08caa5..a53a843987f 100644 --- a/packages/crypto-wallet-core/src/transactions/xrp/index.ts +++ b/packages/crypto-wallet-core/src/transactions/xrp/index.ts @@ -1,13 +1,11 @@ import { createHash } from 'crypto'; import * as xrpl from 'xrpl'; +import * as RBC from 'xrpl/node_modules/ripple-binary-codec'; +import * as binary from 'xrpl/node_modules/ripple-binary-codec/dist/binary'; +import { HashPrefix } from 'xrpl/node_modules/ripple-binary-codec/dist/hash-prefixes'; +import { BTCTxProvider } from '../btc'; import type { Key } from '../../types/derivation'; -enum HashPrefix { - // transaction plus signature to give transaction ID - livenet = 0x54584e00, - mainnet = 0x54584e00, - testnet = 0x73747800 -} export class XRPTxProvider { create(params: { recipients: Array<{ address: string; amount: string; tag?: number }>; @@ -83,21 +81,25 @@ export class XRPTxProvider { return signedTransaction; } - getHash(params: { tx: string; network?: string }): string { - const { tx, network = 'mainnet' } = params; - const prefix = HashPrefix[network].toString(16).toUpperCase(); + getHash(params: { tx: string }): string { + const { tx } = params; + const prefix = HashPrefix.transactionID.toString('hex').toUpperCase(); return this.sha512Half(prefix + tx); } - applySignature(params: { tx: string; signature: string }): string { - const { signature } = params; - return signature; + applySignature(params: { tx: string; signature: string; pubKey: string; }): string { + const { tx, signature, pubKey } = params; + const txJSON = (xrpl.decode(tx) as any) as xrpl.Transaction; + txJSON.TxnSignature = signature; + txJSON.SigningPubKey = pubKey; + const signedTx = xrpl.encode(txJSON); + return signedTx; } sign(params: { tx: string; key: Key }): string { const { tx, key } = params; const signature = this.getSignature({ tx, key }); - return this.applySignature({ tx, signature }); + return this.applySignature({ tx, signature, pubKey: key.pubKey }); } sha512Half(hex: string): string { @@ -107,4 +109,20 @@ export class XRPTxProvider { .toUpperCase() .slice(0, 64); } + + transformSignatureObject(params: { obj: any; }) { + const { obj } = params; + return new BTCTxProvider().transformSignatureObject({ obj }); + } + + getSighash(params: { tx: string; pubKey: string }) { + const { tx, pubKey } = params; + const decoded = RBC.decode(tx); + decoded.SigningPubKey = pubKey; + const encoded = binary.serializeObject(decoded, { + prefix: HashPrefix.transactionSig, + signingFieldsOnly: true + }).toString('hex'); + return this.sha512Half(encoded); + } } From 5ff506f5f24ff29343d98b7d1c436289244be5b4 Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Tue, 2 Sep 2025 16:23:23 -0400 Subject: [PATCH 2/4] lint --- packages/bitcore-cli/src/commands/txproposals.ts | 2 +- packages/bitcore-cli/src/tss.ts | 2 +- packages/bitcore-cli/src/wallet.ts | 8 +++----- .../bitcore-wallet-service/src/lib/chain/xrp/index.ts | 2 +- packages/crypto-wallet-core/src/constants/index.ts | 2 +- packages/crypto-wallet-core/src/transactions/xrp/index.ts | 2 +- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/bitcore-cli/src/commands/txproposals.ts b/packages/bitcore-cli/src/commands/txproposals.ts index 4eccb0d3fe0..3ef4607b5ed 100755 --- a/packages/bitcore-cli/src/commands/txproposals.ts +++ b/packages/bitcore-cli/src/commands/txproposals.ts @@ -1,11 +1,11 @@ import * as prompt from '@clack/prompts'; import fs from 'fs'; import os from 'os'; +import { ITokenObj } from '../../types/wallet'; import type { CommonArgs } from '../../types/cli'; import { UserCancelled } from '../errors'; import { getAction, getFileName } from '../prompts'; import { Utils } from '../utils'; -import { ITokenObj } from 'types/wallet'; export function command(args: CommonArgs) { const { wallet, program } = args; diff --git a/packages/bitcore-cli/src/tss.ts b/packages/bitcore-cli/src/tss.ts index 053795c0628..65c48101701 100644 --- a/packages/bitcore-cli/src/tss.ts +++ b/packages/bitcore-cli/src/tss.ts @@ -1,6 +1,6 @@ import * as prompt from '@clack/prompts'; import { TssSign } from 'bitcore-wallet-client'; -import { type Types as CWCTypes, Transactions } from 'crypto-wallet-core'; +import { Transactions, type Types as CWCTypes } from 'crypto-wallet-core'; import url from 'url'; import { type TssKeyType, diff --git a/packages/bitcore-cli/src/wallet.ts b/packages/bitcore-cli/src/wallet.ts index bd653c43ad9..1dac8eb03fb 100644 --- a/packages/bitcore-cli/src/wallet.ts +++ b/packages/bitcore-cli/src/wallet.ts @@ -11,24 +11,22 @@ import { Utils as BWCUtils } from 'bitcore-wallet-client'; import { + BitcoreLib, Message, + Transactions, type Types as CWCTypes, Utils as CWCUtils, - Web3, - BitcoreLib, - Transactions + Web3 } from 'crypto-wallet-core'; import fs from 'fs'; import path from 'path'; import url from 'url'; -import crypto from 'crypto'; import type { ClientType, ITokenObj, IWallet, KeyType, TssKeyType, - TssSigType, WalletData } from '../types/wallet'; import { Constants } from './constants'; 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 d0e0a033bac..27c05b5f979 100644 --- a/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts +++ b/packages/bitcore-wallet-service/src/lib/chain/xrp/index.ts @@ -1,4 +1,4 @@ -import { Transactions, Validation, BitcoreLib, Deriver } from 'crypto-wallet-core'; +import { BitcoreLib, Deriver, Transactions, Validation } from 'crypto-wallet-core'; import _ from 'lodash'; import { IWallet } from 'src/lib/model'; import { IAddress } from 'src/lib/model/address'; diff --git a/packages/crypto-wallet-core/src/constants/index.ts b/packages/crypto-wallet-core/src/constants/index.ts index 5680e2c5883..06afac758d1 100644 --- a/packages/crypto-wallet-core/src/constants/index.ts +++ b/packages/crypto-wallet-core/src/constants/index.ts @@ -6,9 +6,9 @@ import { EVM_CHAIN_NETWORK_TO_CHAIN_ID, EVM_CHAINS, MULTISIG_CHAINS, + RIPPLE_CHAINS, SVM_CHAINS, UTXO_CHAINS, - RIPPLE_CHAINS, } from './chains'; import { FEE_MINIMUMS } from './feeMinimums'; import { TOKEN_OPTS as opts } from './tokens'; diff --git a/packages/crypto-wallet-core/src/transactions/xrp/index.ts b/packages/crypto-wallet-core/src/transactions/xrp/index.ts index a53a843987f..23127fb80bc 100644 --- a/packages/crypto-wallet-core/src/transactions/xrp/index.ts +++ b/packages/crypto-wallet-core/src/transactions/xrp/index.ts @@ -3,8 +3,8 @@ import * as xrpl from 'xrpl'; import * as RBC from 'xrpl/node_modules/ripple-binary-codec'; import * as binary from 'xrpl/node_modules/ripple-binary-codec/dist/binary'; import { HashPrefix } from 'xrpl/node_modules/ripple-binary-codec/dist/hash-prefixes'; -import { BTCTxProvider } from '../btc'; import type { Key } from '../../types/derivation'; +import { BTCTxProvider } from '../btc'; export class XRPTxProvider { create(params: { From 4555b90842e5bfb0f4dd6c7ff61198e504b1ba5b Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Wed, 3 Sep 2025 09:59:27 -0400 Subject: [PATCH 3/4] cleanup full Response objects in logs --- .../bitcore-cli/src/commands/txproposals.ts | 2 +- .../bitcore-wallet-service/src/lib/server.ts | 172 ++++++++++-------- 2 files changed, 95 insertions(+), 79 deletions(-) diff --git a/packages/bitcore-cli/src/commands/txproposals.ts b/packages/bitcore-cli/src/commands/txproposals.ts index 3ef4607b5ed..8b034884428 100755 --- a/packages/bitcore-cli/src/commands/txproposals.ts +++ b/packages/bitcore-cli/src/commands/txproposals.ts @@ -1,8 +1,8 @@ import * as prompt from '@clack/prompts'; import fs from 'fs'; import os from 'os'; -import { ITokenObj } from '../../types/wallet'; import type { CommonArgs } from '../../types/cli'; +import { ITokenObj } from '../../types/wallet'; import { UserCancelled } from '../errors'; import { getAction, getFileName } from '../prompts'; import { Utils } from '../utils'; diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index 36edec479ba..8ce04b3b371 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -473,7 +473,23 @@ export class WalletService implements IWalletService { this.lock.runLocked(this.walletId, { waitTime }, cb, task); } + + _cleanLogArgs(args) { + if (!args || args.length === 0) { + return []; + } + if (!Array.isArray(args)) { + args = [args]; + } + for (let i = 0; i < args.length; i++) { + args[i] = args[i]?.response ? JSON.parse(JSON.stringify(args[i])) : args[i]; + } + return args; + } + logi(message, ...args) { + args = this._cleanLogArgs(args); + if (typeof message === 'string' && args.length > 0 && !message.endsWith('%o')) { for (let i = 0; i < args.length; i++) { message += ' %o'; @@ -489,6 +505,8 @@ export class WalletService implements IWalletService { } logw(message, ...args) { + args = this._cleanLogArgs(args); + if (typeof message === 'string' && args.length > 0 && !message.endsWith('%o')) { for (let i = 0; i < args.length; i++) { message += ' %o'; @@ -505,6 +523,8 @@ export class WalletService implements IWalletService { } logd(message, ...args) { + args = this._cleanLogArgs(args); + if (typeof message === 'string' && args.length > 0 && !message.endsWith('%o')) { for (let i = 0; i < args.length; i++) { message += ' %o'; @@ -1548,7 +1568,7 @@ export class WalletService implements IWalletService { wallet, err2 => { if (err2) { - this.logw('Error syncing v8 addresses: ', err2); + this.logw('Error syncing v8 addresses:', err2); } return cb(null, isDuplicate); }, @@ -1574,7 +1594,7 @@ export class WalletService implements IWalletService { try { address = wallet.createAddress(!!opts.isChange); } catch (e) { - this.logw('Error creating address', e); + this.logw('Error creating address:', e); return cb('Bad xPub'); } @@ -1729,7 +1749,7 @@ export class WalletService implements IWalletService { try { bc = BlockChainExplorer(opts); } catch (ex) { - this.logw('Could not instantiate blockchain explorer', ex); + this.logw('Could not instantiate blockchain explorer:', ex); } return bc; } @@ -2092,7 +2112,7 @@ export class WalletService implements IWalletService { if (!bc) return cb(new Error('Could not get blockchain explorer instance')); bc.estimateFee(points, (err, result) => { if (err) { - this.logw('Error estimating fee', err); + this.logw('Error estimating fee:', err); return cb(err); } @@ -2125,7 +2145,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.estimateFeeV2(opts, (err, result) => { if (err) { - this.logw('Error estimating fee', err); + this.logw('Error estimating fee:', err); return reject(err); } return resolve(result); @@ -2140,7 +2160,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.estimatePriorityFee(opts, (err, result) => { if (err) { - this.logw('Error estimating priority fee', err); + this.logw('Error estimating priority fee:', err); return reject(err); } return resolve(result); @@ -2224,7 +2244,7 @@ export class WalletService implements IWalletService { this._sampleFeeLevels(opts.chain, opts.network, samplePoints(), (err, feeSamples, failed) => { if (err) { if (oldvalues) { - this.logw('## There was an error estimating fees... using old cached values'); + this.logw('## There was an error estimating fees. Using old cached values. Error:', err); return cb(null, oldvalues, true); } } @@ -2254,13 +2274,13 @@ export class WalletService implements IWalletService { } if (failed > 0) { - this.logw('Not caching default values. Failed:' + failed); + this.logw('Not caching default values. Failed: ' + failed); return cb(null, values); } this.storage.storeGlobalCache(cacheKey, values, err => { if (err) { - this.logw('Could not store fee level cache'); + this.logw('Could not store fee level cache:', err); } return cb(null, values); }); @@ -2447,7 +2467,7 @@ export class WalletService implements IWalletService { if (err) return cb(err); const level = levels.find(l => l.level === opts.feeLevel); if (!level) { - const msg = 'Could not compute fee for "' + opts.feeLevel + '" level'; + const msg = `Could not compute fee for "${opts.feeLevel}" level`; this.logw(msg); return cb(new ClientError(msg)); } @@ -2461,7 +2481,7 @@ export class WalletService implements IWalletService { if (!bc) return cb(new Error('Could not get blockchain explorer instance')); bc.getTransactionCount(address, (err, nonce) => { if (err) { - this.logw('Error estimating nonce', err); + this.logw('Error estimating nonce:', err); return cb(err); } return cb(null, nonce); @@ -2474,7 +2494,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getTransactionCount(opts.address, (err, nonce) => { if (err) { - this.logw('Error estimating nonce', err); + this.logw('Error estimating nonce:', err); return reject(err); } return resolve(nonce); @@ -2488,7 +2508,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.estimateGas(opts, (err, gasLimit) => { if (err) { - this.logw('Error estimating gas limit', err); + this.logw('Error estimating gas limit:', err); return reject(err); } return resolve(gasLimit); @@ -2502,7 +2522,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getMultisigContractInstantiationInfo(opts, (err, contractInstantiationInfo) => { if (err) { - this.logw('Error getting contract instantiation info', err); + this.logw('Error getting contract instantiation info:', err); return reject(err); } return resolve(contractInstantiationInfo); @@ -2516,7 +2536,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getMultisigContractInfo(opts, (err, contractInfo) => { if (err) { - this.logw('Error getting contract instantiation info', err); + this.logw('Error getting contract instantiation info:', err); return reject(err); } return resolve(contractInfo); @@ -2530,7 +2550,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getTokenContractInfo(opts, (err, contractInfo) => { if (err) { - this.logw('Error getting contract info', err); + this.logw('Error getting contract info:', err); return reject(err); } return resolve(contractInfo); @@ -2544,7 +2564,7 @@ export class WalletService implements IWalletService { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getMultisigTxpsInfo(opts, (err, multisigTxpsInfo) => { if (err) { - this.logw('Error getting contract txps hash', err); + this.logw('Error getting contract txps hash:', err); return reject(err); } return resolve(multisigTxpsInfo); @@ -2956,7 +2976,7 @@ export class WalletService implements IWalletService { this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => { if (err) { - this.logw('Error fetching tx note for ' + txp.txid); + this.logw(`Error fetching tx note for ${txp.txid}:`, err); } txp.note = note; return cb(null, txp); @@ -2979,7 +2999,7 @@ export class WalletService implements IWalletService { this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => { if (err) { - this.logw('Error fetching tx note for ' + txp.txid); + this.logw(`Error fetching tx note for ${txp.txid}:`, err); } txp.note = note; return cb(null, txp); @@ -3103,7 +3123,7 @@ export class WalletService implements IWalletService { if (!bc) return cb(new Error('Could not get blockchain explorer instance')); bc.broadcast(raw, (err, txid) => { if (err) { - logger.info('Error broadcasting tx: %o %o %o %o', chain, network, raw, err); + this.logw('Error broadcasting tx: %o %o %o %o', chain, network, raw, err); return cb(err); } return cb(null, txid); @@ -3196,7 +3216,8 @@ export class WalletService implements IWalletService { const copayer = wallet.getCopayer(this.copayerId); try { - if (!txp.sign(this.copayerId, opts.signatures, copayer.xPubKey)) { + const xPubKey = wallet.tssKeyId ? wallet.clientDerivedPublicKey : copayer.xPubKey; + if (!txp.sign(this.copayerId, opts.signatures, xPubKey)) { this.logw('Error signing transaction (BAD_SIGNATURES)'); this.logw('Client version:', this.clientVersion); this.logw('Arguments:', JSON.stringify(opts)); @@ -3206,7 +3227,7 @@ export class WalletService implements IWalletService { return cb(Errors.BAD_SIGNATURES); } } catch (ex) { - this.logw('Error signing transaction proposal', ex); + this.logw('Error signing transaction proposal:', ex); return cb(ex); } @@ -3280,67 +3301,62 @@ export class WalletService implements IWalletService { return cb(Err); } - this.getTx( - { - txProposalId: opts.txProposalId - }, - (err, txp) => { - if (err) return cb(err); + this.getTx({ txProposalId: opts.txProposalId}, (err, txp) => { + if (err) return cb(err); - if (txp.status == 'broadcasted') return cb(Errors.TX_ALREADY_BROADCASTED); - if (txp.status != 'accepted') return cb(Errors.TX_NOT_ACCEPTED); + if (txp.status == 'broadcasted') return cb(Errors.TX_ALREADY_BROADCASTED); + if (txp.status != 'accepted') return cb(Errors.TX_NOT_ACCEPTED); - const sub = TxConfirmationSub.create({ - copayerId: txp.creatorId, - txid: txp.txid, - walletId: txp.walletId, - amount: txp.amount, - isActive: true, - isCreator: true - }); - this.storage.storeTxConfirmationSub(sub, err => { - if (err) logger.error('Could not store Tx confirmation subscription: %o', err); + const sub = TxConfirmationSub.create({ + copayerId: txp.creatorId, + txid: txp.txid, + walletId: txp.walletId, + amount: txp.amount, + isActive: true, + isCreator: true + }); + this.storage.storeTxConfirmationSub(sub, err => { + if (err) logger.error('Could not store Tx confirmation subscription: %o', err); - let raw; - try { - raw = txp.getRawTx(); - } catch (ex) { - return cb(ex); - } - this._broadcastRawTx(wallet.chain, wallet.network, raw, (err, txid) => { - if (err || txid != txp.txid) { - logger.warn('Broadcast failed: %o %o %o %o %o', wallet.id, wallet.chain, wallet.network, raw, err?.stack || err?.message || err); + let raw; + try { + raw = txp.getRawTx(); + } catch (ex) { + return cb(ex); + } + this._broadcastRawTx(wallet.chain, wallet.network, raw, (err, txid) => { + if (err || txid != txp.txid) { + const broadcastErr = err?.response ? this._cleanLogArgs(err)[0] : err?.stack || err?.message || err; + this.logw('Broadcast failed: %o %o %o %o %o', wallet.id, wallet.chain, wallet.network, raw, broadcastErr); - const broadcastErr = err; - // Check if tx already in blockchain - this._checkTxInBlockchain(txp, (err, isInBlockchain) => { - if (err) return cb(err); - if (!isInBlockchain) return cb(broadcastErr || 'broadcast error'); + // Check if tx already in blockchain + this._checkTxInBlockchain(txp, (err, isInBlockchain) => { + if (err) return cb(err); + if (!isInBlockchain) return cb(broadcastErr || 'broadcast error'); - this._processBroadcast( - txp, - { - byThirdParty: true - }, - cb - ); - }); - } else { this._processBroadcast( txp, { - byThirdParty: false + byThirdParty: true }, - err => { - if (err) return cb(err); - return cb(null, txp); - } + cb ); - } - }); + }); + } else { + this._processBroadcast( + txp, + { + byThirdParty: false + }, + err => { + if (err) return cb(err); + return cb(null, txp); + } + ); + } }); - } - ); + }); + }); }); } @@ -3734,7 +3750,7 @@ export class WalletService implements IWalletService { this.storage.storeGlobalCache(cacheKey, values, err => { if (err) { - this.logw('Could not store bc heigth cache'); + this.logw('Could not store bc height cache:', err); } return cb(null, values.current, values.hash); }); @@ -3784,7 +3800,7 @@ export class WalletService implements IWalletService { bc.getCheckData(wallet, (err, serverCheck) => { // If there is an error, just ignore it (server does not support walletCheck) if (err) { - this.logw('Error at bitcore WalletCheck, ignoring' + err); + this.logw('Error at bitcore WalletCheck, ignoring:', err); return cb(); } @@ -4156,7 +4172,7 @@ export class WalletService implements IWalletService { } if (result) { - this.logw('Advert already exists'); + this.logw('Advert already exists:', opts.adId); return cb(null, adId); } }); @@ -4215,7 +4231,7 @@ export class WalletService implements IWalletService { }, (err, levels) => { if (err) { - this.logw('Could not fetch fee levels', err); + this.logw('Could not fetch fee levels:', err); } else { const level = levels.find(l => l.level === 'superEconomy'); if (!level || !level.nbBlocks) { @@ -4517,7 +4533,7 @@ export class WalletService implements IWalletService { return tx; }); this.tagLowFeeTxs(wallet, finalTxs, err => { - if (err) this.logw('Failed to tag unconfirmed with low fee'); + if (err) this.logw('Failed to tag unconfirmed with low fee:', err); if (res.txs.fromCache) { let p = ''; @@ -5410,7 +5426,7 @@ export class WalletService implements IWalletService { }, (err, data) => { if (err) { - this.logw('An error occured while retrieving the token rates', err); + this.logw('An error occured while retrieving the token rates:', err); return reject(err.body ?? err); } else { if (!data?.body) { From 4a89b797b55310b402dc1909c170fa10b9a3e92c Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Tue, 30 Sep 2025 13:38:00 -0400 Subject: [PATCH 4/4] fix test --- packages/bitcore-wallet-service/src/lib/server.ts | 2 +- packages/crypto-wallet-core/package.json | 2 +- packages/crypto-wallet-core/src/transactions/xrp/index.ts | 3 ++- packages/crypto-wallet-core/test/transactions.test.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index 3e7db41dc0e..a56feed6c3c 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -2135,7 +2135,7 @@ export class WalletService implements IWalletService { if (failed.length) { const logger = network == 'livenet' ? this.logw : this.logi; - logger('Could not compute fee estimation in ' + network + ': ' + failed.join(', ') + ' blocks.'); + logger.call(this, 'Could not compute fee estimation in ' + network + ': ' + failed.join(', ') + ' blocks.'); } return cb(null, levels, failed.length); diff --git a/packages/crypto-wallet-core/package.json b/packages/crypto-wallet-core/package.json index 1f6283af349..f9ceb3b5369 100644 --- a/packages/crypto-wallet-core/package.json +++ b/packages/crypto-wallet-core/package.json @@ -11,7 +11,7 @@ "precommit": "npm run fix", "lint": "tslint -c ../../tslint.json 'src/**/*.ts'", "fix": "tslint --fix -c ../../tslint.json 'src/**/*.ts'", - "test": "npm run compile && mocha -r tsx test/**/*.test.ts", + "test": "npm run compile && mocha -r tsx 'test/**/*.test.ts'", "pub": "npm run compile && npm publish" }, "keywords": [ diff --git a/packages/crypto-wallet-core/src/transactions/xrp/index.ts b/packages/crypto-wallet-core/src/transactions/xrp/index.ts index 23127fb80bc..2255ad359b0 100644 --- a/packages/crypto-wallet-core/src/transactions/xrp/index.ts +++ b/packages/crypto-wallet-core/src/transactions/xrp/index.ts @@ -78,7 +78,8 @@ export class XRPTxProvider { getSignature(params: { tx: string; key: Key }): string { const { signedTransaction } = this.getSignatureObject(params); - return signedTransaction; + const decoded = (xrpl.decode(signedTransaction) as any) as xrpl.Transaction; + return decoded.TxnSignature; } getHash(params: { tx: string }): string { diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index 30415d3718c..8433de46915 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -1688,7 +1688,7 @@ describe('Transaction', function() { tx: '120000228000000024000000012E0001E2405011101234567890123456789012345671012345678901234567890156789012345661400000000001E24068400000000000000C732103DBEEC5E9E76DA09C5B502A67136BC2D73423E8902A7C35A8CBC0C5A6AC0469E874473045022100D5C19360E77D691A11CA693F6E8D8472DA6749D16A06E072ED1110EB3FD9E2C80220169F95E55943C3575CEAA46413FE660E4F8F2E7158FAC235DC3CB9C9F26918098114A2C8E8CD9A9133CAD90F2668159AAF572612A5028314A2C8E8CD9A9133CAD90F2668159AAF572612A502' }); - const expectedHash = 'E3DA85BC1EEE51F7A6CF93AE170CC17A9FB8751EEB3EE37C938C9E5AD261E55E'; + const expectedHash = '61EA6DF3BD1E435283BA0B06311C7BA683A32A80E465196D9F16A23A439EF6F4'; expect(hash.length).to.equal(64); expect(hash).to.equal(expectedHash); });