Skip to content

Consistent UTXO Handling for CWC Transactions - #4213

Open
MicahMaphet wants to merge 11 commits into
bitpay:masterfrom
MicahMaphet:uni-tx-create
Open

Consistent UTXO Handling for CWC Transactions#4213
MicahMaphet wants to merge 11 commits into
bitpay:masterfrom
MicahMaphet:uni-tx-create

Conversation

@MicahMaphet

@MicahMaphet MicahMaphet commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description 🗒️

Previously crypto-wallet-core's CWC.Transactions had inconsistent handling of utxos. .create and .sign required bitcore-node utxos. By bitcore-node utxos I mean how bitcore-node stores utxos in its database. These have mintTxid, spentTxid, mintIndex, and value; while bitcore-lib style utxos have txid, outputIndex, and satoshis. .getSighash required bitcore-lib utxos.

Now CWC.Transactions accepts both kinds of utxos.

Motivation

I need these changes for bitcore-hardware in order to sign transactions. I need to construct a transaction with .create and pass those utxos into a [a given hardware wallet object].sign. To sign with hardware wallets, I need to give the the sighash via .getSighash. The current version of crypto-wallet-core does not allow this because .create and .getSighash take different utxo types. This problem could be most easily solved be simply making .getSighash convert bitcore-node utxos into bitcore-lib utxos. However, we want consistent handling of utxos. This PR instead solves the core problem by making all transaction methods in the cryto-wallet-core utxo chains accept both bitcore-node and bitcore-lib utxos.

Changelog 🪵

functionality 🔨

  • Defined two utxo types for utxo chains (bitcoin, bitcoin cash, litecoin, and doge):
    EveryUtxoType where the properties are unknown due to it being received outside the Transactions class. Can contain any type of utxo data: UnspentOutput, UnspentOutput.toObject, and utxos from the bitcore-node database. Hard to work with because all the properties need to be checked individually.
    UtxoType for internal usage. Nice to work with because the properties don't need to be checked. Uses property naming from UnspentOutput.
  • Transactions.create, Transactions.sign, Transactions.getSighash, and Transactions.getSigningAddresses accept EveryUtxoType
  • Transactions.create silently ignores utxo mintHeight sorting when supplied with bitcore-lib utxos

organization 🎶

  • All functions in utxo Transaction classes have parameter and return types
  • Most functions use 'const { ... } = params;'
  • When the type is 'number | string', Number() is used instead of parseInt()
  • getRelatedUtxos accepts UtxoType rather than bitcore-node utxo types

Testing Notes 🥼 🧪

crypto-wallet-core tests all work, but I can add more to test the new functionality.

The following cases would previously break, but they now work. .create and .getSighash previously use different utxo types but now accept either.

bitcore-lib utxos
const CWC = require('@bitpay-labs/crypto-wallet-core');
const { UnspentOutput } = CWC.BitcoreLib.Transaction;

const utxos = [
  new UnspentOutput({
    outputIndex: 1,
    txid: '6bcb6a3695e24b90d14b8dcbdbb3280a9f5fadb408cd76093a078db6fe4a6f24',
    script: '76a91403b6029fe9863d8c2e4e42ca2c08e55c69dd060188ac',
    amount: 0.002000000,
  }),
  new UnspentOutput({
    outputIndex: 1,
    txid: '5b9f304f363c98ac9270773b9b567b0d5b2f2b0d522120abe7e551584e3c3244',
    script: '76a914b38845dcfc6911d96a43b4c6ec27bc741bdc005588ac',
    amount: 2.000000000,
  })
];

const tx = CWC.Transactions.create({
  chain: 'BTC',
  recipients: [{ address: 'bcrt1qp8eln5e22s4qhyrkcrzeef9l68mfds0fwn82tc', amount: 200_100_000 }],
  utxos,
  isSweep: true
});

const sighash = CWC.Transactions.getSighash({ chain: 'BTC', tx, utxos, index: 0 });
console.log(sighash);
bitcore-node utxos
const CWC = require('@bitpay-labs/crypto-wallet-core');

const utxos = [
  {
    mintIndex: 1,
    mintTxid: '6bcb6a3695e24b90d14b8dcbdbb3280a9f5fadb408cd76093a078db6fe4a6f24',
    script: '76a91403b6029fe9863d8c2e4e42ca2c08e55c69dd060188ac',
    value: 200_000,
  },
  {
    mintIndex: 1,
    mintTxid: '5b9f304f363c98ac9270773b9b567b0d5b2f2b0d522120abe7e551584e3c3244',
    script: '76a914b38845dcfc6911d96a43b4c6ec27bc741bdc005588ac',
    value: 200_000_000,
  }
];

const tx = CWC.Transactions.create({
  chain: 'BTC',
  recipients: [{ address: 'bcrt1qp8eln5e22s4qhyrkcrzeef9l68mfds0fwn82tc', amount: 200_100_000 }],
  utxos,
  isSweep: true
});

const sighash = CWC.Transactions.getSighash({ chain: 'BTC', tx, utxos, index: 0 });
console.log(sighash);

Checklist ✅ 🗒️

  • I have read CONTRIBUTING.md and verified that this PR follows the guidelines and requirements outlined in it.
  • Add tests for new functionality

@MicahMaphet MicahMaphet changed the title Consistent UTXO Handling for Transaction create, sign, getSighash, and getSigningAddresses Consistent UTXO Handling for CWC Transactions Aug 7, 2026

@kajoseph kajoseph left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hey @MicahMaphet , thanks for the PR and nice catch. I think a better way to approach this is to have a standardizeUtxo function that looks something like this:

standardizeUtxo(utxo) {
  return {
    txid: utxo.txid || utxo.mintTxid,
    vout: utxo.vout || utxo.mintIndex,
    ...etc...
  };
}

then, we can just do

utxos = utxos.map(this.standardizeUtxo);

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved issues remain in UTXO validation, public typings, address normalization, and API exposure.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Standardizes UTXO handling across BTC, BCH, DOGE, and LTC transaction operations.

Changes:

  • Supports bitcore-node and bitcore-lib UTXO formats.
  • Updates transaction creation, signing, and sighash handling.
  • Adds cross-chain UTXO compatibility tests.
File summaries
File Summary
packages/crypto-wallet-core/test/transactions.test.ts Tests UTXO format compatibility across chains.
packages/crypto-wallet-core/src/transactions/ltc/index.ts Integrates normalized UTXOs for Litecoin transactions.
packages/crypto-wallet-core/src/transactions/doge/index.ts Integrates normalized UTXOs for Dogecoin transactions.
packages/crypto-wallet-core/src/transactions/btc/index.ts Adds UTXO normalization and updated transaction handling.
packages/crypto-wallet-core/src/transactions/bch/index.ts Integrates normalized UTXOs for Bitcoin Cash transactions.
Review details

Suppressed comments (9)

packages/crypto-wallet-core/src/transactions/bch/index.ts:7

  • This override narrows recipient amounts to number, while the base provider and the implementation explicitly support number | string (Number(recipient.amount)). String amounts therefore fail type checking for BCH even though they work at runtime; keep this override's public type consistent with the base.
    recipients: Array<{ address: string; amount: number }>;

packages/crypto-wallet-core/src/transactions/btc/index.ts:49

  • The new docstring misspells “internaly”; please correct it to “internally”.
   */

packages/crypto-wallet-core/src/transactions/btc/index.ts:285

  • The new type comment uses “were” where “where” is required, making the description ungrammatical.
 * Utxo type for functions were the received utxo type is unknown.

packages/crypto-wallet-core/src/transactions/btc/index.ts:218

  • This changes the provider's getSigningAddresses implementation, but the exported TransactionsProxy still has no getSigningAddresses forwarding method, so Transactions.getSigningAddresses(...) remains unavailable despite being listed as a supported API in the PR description. Add the proxy method (and its public typing) or remove this as a public API promise.
  getSigningAddresses(params: {
    tx: TransactionType;
    utxos: EveryUtxoType[];
  }): (string | undefined)[] {
    const { tx, utxos } = params;
    const bitcoreTx = new this.lib.Transaction(tx);
    const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo));
    const applicableUtxos = this.getRelatedUtxos({
      outputs: bitcoreTx.inputs,
      utxos: btcUtxos
    });
    return applicableUtxos.map(utxo => utxo.address);

packages/crypto-wallet-core/src/transactions/btc/index.ts:54

  • Defaulting a missing index to 0 silently turns an invalid UTXO into a reference to output 0. A node record with an absent mintIndex can therefore be selected and signed as <txid>:0, whereas the underlying UnspentOutput validation would reject it. Validate that a supported index field is present instead of applying this fallback.
      outputIndex: Number(utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout ?? 0),

packages/crypto-wallet-core/src/transactions/btc/index.ts:271

  • The new documentation uses “use” where “used” is required.
 * Standard utxo type use for internal processing.

packages/crypto-wallet-core/src/transactions/btc/index.ts:52

  • The ?? 0 fallback fabricates a valid zero-satoshi UTXO when all supported amount fields are absent. In isSweep mode coin selection is bypassed, so malformed input can be serialized as a spendable-looking input instead of being rejected. Require a value field and let invalid UTXOs fail validation rather than defaulting to zero.
      satoshis: Number(utxo.satoshis ?? utxo.value ?? this.lib.Unit.fromBTC(utxo.amount ?? 0).toSatoshis()),

packages/crypto-wallet-core/src/transactions/doge/index.ts:7

  • This override narrows recipient amounts to number, while the base provider and the implementation explicitly support number | string (Number(recipient.amount)). String amounts therefore fail type checking for DOGE even though they work at runtime; keep this override's public type consistent with the base.
    recipients: Array<{ address: string; amount: number }>;

packages/crypto-wallet-core/src/transactions/ltc/index.ts:7

  • This override narrows recipient amounts to number, while the base provider and the implementation explicitly support number | string (Number(recipient.amount)). String amounts therefore fail type checking for LTC even though they work at runtime; keep this override's public type consistent with the base.
    recipients: Array<{ address: string; amount: number }>;
  • Files reviewed: 5/5 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/crypto-wallet-core/src/transactions/btc/index.ts Outdated
Comment thread packages/crypto-wallet-core/src/transactions/btc/index.ts
Comment thread packages/crypto-wallet-core/src/transactions/btc/index.ts Outdated
Comment on lines +81 to +86
const unspentOutputUtxos = [
{
txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab',
outputIndex: 1,
satoshis: 90_000,
script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac'
crypto-wallet-core handle empty ("") address for utxo conversion

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants