From 9a8a6b58b5486cf07a27b19ffa54a70dcb8db541 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Thu, 6 Aug 2026 23:33:46 -0400 Subject: [PATCH 1/9] make CWC.Transactions create, sign, and getSighash all accept both bitcore-node and bitcore-lib style transactions --- .../src/transactions/btc/index.ts | 102 ++++++++++++------ 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index e172e8ad8a0..25432214429 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -16,42 +16,44 @@ export class BTCTxProvider { selectCoins( recipients: Array<{ amount: number }>, - utxos: Array<{ - value: number; - mintHeight: number; - txid?: string; - mintTxid?: string; - mintIndex?: number; - }>, + utxos: UtxoType[], fee: number ) { - utxos = utxos.sort(function(a, b) { - return a.mintHeight - b.mintHeight; - }); + // Only sort by block height if utxos are bitcore-node style + if (utxos[0].mintHeight != undefined) { + utxos = utxos.sort(function(a, b) { + return a.mintHeight - b.mintHeight; + }); + } let index = 0; let utxoSum = 0; const recepientSum = recipients.reduce((sum, cur) => sum + Number(cur.amount), fee || 0); while (utxoSum < recepientSum) { const utxo = utxos[index]; - utxoSum += Number(utxo.value); + utxoSum += Number(utxo.value ?? utxo.satoshis); index += 1; } const filteredUtxos = utxos.slice(0, index); return filteredUtxos; } - create({ recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock }) { + create(params: { + recipients: Array<{ address: string; amount: number }>; + utxos: UtxoType[]; + change: string; + feeRate: number; + fee: number; + isSweep: boolean; + replaceByFee: boolean; + lockUntilDate: number; + lockUntilBlock: number; + }) { + const { recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock } = params; const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + const btcUtxos = utxos[0].mintTxid ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; const tx = new this.lib.Transaction().from(btcUtxos); + if (fee) { tx.fee(fee); } @@ -62,7 +64,7 @@ export class BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, parseInt(recipient.amount as any)); } if (replaceByFee && typeof tx.enableRBF === 'function') { tx.enableRBF(); @@ -133,7 +135,7 @@ export class BTCTxProvider { return bitcoreTx.hash; } - sign(params: { tx: string; keys: Array; utxos: any[]; pubkeys?: any[]; threshold?: number; opts: any }) { + sign(params: { tx: string; keys: Array; utxos: UtxoType[]; pubkeys?: any[]; threshold?: number; opts: any }) { const { tx, keys, pubkeys, threshold, opts } = params; const utxos = params.utxos || []; const bitcoreTx = new this.lib.Transaction(tx); @@ -152,17 +154,28 @@ export class BTCTxProvider { return signedTx; } - getRelatedUtxos({ outputs, utxos }) { + /** + * Converts the utxos in a bitcore-nodes database to bitcore lib utxos + * + * @param utxos bitcore-node style utxos + * @returns lib style utxos + */ + nodeToLibUtxos(utxos: NodeUtxoType[]): BitcoreLib.Transaction.UnspentOutput[] { + return utxos.map(utxo => new this.lib.Transaction.UnspentOutput({ + satoshis: utxo.value, + // bitcore-node utxos have both mintTxid and spentTxid + txid: utxo.mintTxid, + outputIndex: utxo.mintIndex, + script: utxo.script, + address: utxo.address + })); + } + + getRelatedUtxos(params: { outputs: any[]; utxos: UtxoType[] }) { + const { outputs, utxos } = params; const txids = outputs.map(output => output.toObject().prevTxId); const applicableUtxos = utxos.filter(utxo => txids.includes(utxo.txid || utxo.mintTxid)); - return applicableUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / Math.pow(10, 8), - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + return utxos[0].mintTxid == undefined ? applicableUtxos : this.nodeToLibUtxos(applicableUtxos); } getOutputsFromTx({ tx }) { @@ -172,7 +185,8 @@ export class BTCTxProvider { }); } - getSigningAddresses({ tx, utxos }): string[] { + getSigningAddresses(params: { tx: string | BitcoreLib.Transaction; utxos: UtxoType[] }): string[] { + const { tx, utxos } = params; const bitcoreTx = new this.lib.Transaction(tx); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, @@ -184,7 +198,7 @@ export class BTCTxProvider { getSighash(params: { tx: string | BitcoreLib.Transaction; index: number; - utxos?: BitcoreLib.Transaction.UnspentOutput[]; + utxos?: UtxoType[]; pubKey?: string | BitcoreLib.PublicKey | BitcoreLib.HDPublicKey; path?: string; sigtype?: number; @@ -204,7 +218,7 @@ export class BTCTxProvider { tx = new this.lib.Transaction(tx); } if (utxos) { - tx.associateInputs(utxos.map(this.lib.Transaction.UnspentOutput), pubKeys, threshold, opts); + tx.associateInputs(utxos[0].mintTxid ? this.nodeToLibUtxos(utxos) : utxos, pubKeys, threshold, opts); } $.checkState(tx.inputs[index].output instanceof this.lib.Transaction.Output, 'Input must have all utxo info'); @@ -224,4 +238,22 @@ export class BTCTxProvider { } } -type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; \ No newline at end of file +type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; +// bitcore-node style utxo minus values that are not used +type NodeUtxoType = { + // network: string; + // chain: string; + mintTxid: string; + mintIndex: number; + mintHeight: number; + // coinbase: boolean; + value: number; + address: string; + script: string; + // spentTxid: string; + // spentHeight?: number; + // confirmations?: number; + // sequenceNumber?: number; +} +// utxo type recieved externaly from this class that could either be from bitcore-node or already a lib utxo +type UtxoType = NodeUtxoType | BitcoreLib.Transaction.UnspentOutput; From 1cb66172cb2627703b1c3e2067ca4f913115ede6 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 7 Aug 2026 10:29:00 -0400 Subject: [PATCH 2/9] apply node lib utxo conversion to bch, ltc, and doge --- .../crypto-wallet-core/src/transactions/bch/index.ts | 9 +-------- .../crypto-wallet-core/src/transactions/doge/index.ts | 9 +-------- .../crypto-wallet-core/src/transactions/ltc/index.ts | 9 +-------- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/bch/index.ts b/packages/crypto-wallet-core/src/transactions/bch/index.ts index fdb3314ab3e..f83ebb968fd 100644 --- a/packages/crypto-wallet-core/src/transactions/bch/index.ts +++ b/packages/crypto-wallet-core/src/transactions/bch/index.ts @@ -5,14 +5,7 @@ export class BCHTxProvider extends BTCTxProvider { lib = BitcoreLibCash; create({ recipients, utxos = [], change, fee = 20000, isSweep }) { const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + const btcUtxos = filteredUtxos[0].mintTxid == undefined ? filteredUtxos : this.nodeToLibUtxos(filteredUtxos); const tx = new this.lib.Transaction().from(btcUtxos).feePerByte(Number(fee) + 2); if (change) { tx.change(change); diff --git a/packages/crypto-wallet-core/src/transactions/doge/index.ts b/packages/crypto-wallet-core/src/transactions/doge/index.ts index b4e97280056..d6510ea3999 100644 --- a/packages/crypto-wallet-core/src/transactions/doge/index.ts +++ b/packages/crypto-wallet-core/src/transactions/doge/index.ts @@ -5,14 +5,7 @@ export class DOGETxProvider extends BTCTxProvider { lib = BitcoreLibDoge; create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + const btcUtxos = filteredUtxos[0].mintTxid == undefined ? filteredUtxos : this.nodeToLibUtxos(filteredUtxos); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); diff --git a/packages/crypto-wallet-core/src/transactions/ltc/index.ts b/packages/crypto-wallet-core/src/transactions/ltc/index.ts index 41cd1a5ef8b..653a0379379 100644 --- a/packages/crypto-wallet-core/src/transactions/ltc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/ltc/index.ts @@ -5,14 +5,7 @@ export class LTCTxProvider extends BTCTxProvider { lib = BitcoreLibLtc; create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + const btcUtxos = filteredUtxos[0].mintTxid == undefined ? filteredUtxos : this.nodeToLibUtxos(filteredUtxos); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); From 01c55ff006abdd5cf665bd613f8a6eb170590e18 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 7 Aug 2026 12:32:57 -0400 Subject: [PATCH 3/9] standardize utxo type differentiation with isNodeUtxo --- .../src/transactions/bch/index.ts | 2 +- .../src/transactions/btc/index.ts | 19 ++++++++++++++----- .../src/transactions/doge/index.ts | 2 +- .../src/transactions/ltc/index.ts | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/bch/index.ts b/packages/crypto-wallet-core/src/transactions/bch/index.ts index f83ebb968fd..42cae047c51 100644 --- a/packages/crypto-wallet-core/src/transactions/bch/index.ts +++ b/packages/crypto-wallet-core/src/transactions/bch/index.ts @@ -5,7 +5,7 @@ export class BCHTxProvider extends BTCTxProvider { lib = BitcoreLibCash; create({ recipients, utxos = [], change, fee = 20000, isSweep }) { const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos[0].mintTxid == undefined ? filteredUtxos : this.nodeToLibUtxos(filteredUtxos); + const btcUtxos = this.isNodeUtxo(filteredUtxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; const tx = new this.lib.Transaction().from(btcUtxos).feePerByte(Number(fee) + 2); if (change) { tx.change(change); diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index 25432214429..641caaea12f 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -20,7 +20,7 @@ export class BTCTxProvider { fee: number ) { // Only sort by block height if utxos are bitcore-node style - if (utxos[0].mintHeight != undefined) { + if (this.isNodeUtxo(utxos[0])) { utxos = utxos.sort(function(a, b) { return a.mintHeight - b.mintHeight; }); @@ -51,9 +51,8 @@ export class BTCTxProvider { }) { const { recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock } = params; const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = utxos[0].mintTxid ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; + const btcUtxos = this.isNodeUtxo(utxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; const tx = new this.lib.Transaction().from(btcUtxos); - if (fee) { tx.fee(fee); } @@ -171,11 +170,21 @@ export class BTCTxProvider { })); } + /** + * Return true if utxo is a bitcore-node utxo + * + * @param utxo either a bitcore-lib or bitcore-node utxo + * @returns true if node utxo + */ + isNodeUtxo(utxo: UtxoType): boolean { + return utxo.mintTxid != undefined; + } + getRelatedUtxos(params: { outputs: any[]; utxos: UtxoType[] }) { const { outputs, utxos } = params; const txids = outputs.map(output => output.toObject().prevTxId); const applicableUtxos = utxos.filter(utxo => txids.includes(utxo.txid || utxo.mintTxid)); - return utxos[0].mintTxid == undefined ? applicableUtxos : this.nodeToLibUtxos(applicableUtxos); + return this.isNodeUtxo(utxos[0]) ? this.nodeToLibUtxos(applicableUtxos) : applicableUtxos; } getOutputsFromTx({ tx }) { @@ -218,7 +227,7 @@ export class BTCTxProvider { tx = new this.lib.Transaction(tx); } if (utxos) { - tx.associateInputs(utxos[0].mintTxid ? this.nodeToLibUtxos(utxos) : utxos, pubKeys, threshold, opts); + tx.associateInputs(this.isNodeUtxo(utxos[0]) ? this.nodeToLibUtxos(utxos) : utxos, pubKeys, threshold, opts); } $.checkState(tx.inputs[index].output instanceof this.lib.Transaction.Output, 'Input must have all utxo info'); diff --git a/packages/crypto-wallet-core/src/transactions/doge/index.ts b/packages/crypto-wallet-core/src/transactions/doge/index.ts index d6510ea3999..a53c4043cfa 100644 --- a/packages/crypto-wallet-core/src/transactions/doge/index.ts +++ b/packages/crypto-wallet-core/src/transactions/doge/index.ts @@ -5,7 +5,7 @@ export class DOGETxProvider extends BTCTxProvider { lib = BitcoreLibDoge; create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos[0].mintTxid == undefined ? filteredUtxos : this.nodeToLibUtxos(filteredUtxos); + const btcUtxos = this.isNodeUtxo(filteredUtxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); diff --git a/packages/crypto-wallet-core/src/transactions/ltc/index.ts b/packages/crypto-wallet-core/src/transactions/ltc/index.ts index 653a0379379..1c28dd3fdb8 100644 --- a/packages/crypto-wallet-core/src/transactions/ltc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/ltc/index.ts @@ -5,7 +5,7 @@ export class LTCTxProvider extends BTCTxProvider { lib = BitcoreLibLtc; create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos[0].mintTxid == undefined ? filteredUtxos : this.nodeToLibUtxos(filteredUtxos); + const btcUtxos = this.isNodeUtxo(filteredUtxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); From b72064e92a14c0234b4993ff8e625bcfece5cf63 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 7 Aug 2026 15:17:07 -0400 Subject: [PATCH 4/9] refactored CWC.Transactions utxo handling with standard and external utxo types --- .../src/transactions/bch/index.ts | 2 +- .../src/transactions/btc/index.ts | 120 ++++++++++-------- .../src/transactions/doge/index.ts | 2 +- .../src/transactions/ltc/index.ts | 2 +- 4 files changed, 68 insertions(+), 58 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/bch/index.ts b/packages/crypto-wallet-core/src/transactions/bch/index.ts index 42cae047c51..d89fca223b4 100644 --- a/packages/crypto-wallet-core/src/transactions/bch/index.ts +++ b/packages/crypto-wallet-core/src/transactions/bch/index.ts @@ -5,7 +5,7 @@ export class BCHTxProvider extends BTCTxProvider { lib = BitcoreLibCash; create({ recipients, utxos = [], change, fee = 20000, isSweep }) { const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = this.isNodeUtxo(filteredUtxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; + const btcUtxos = filteredUtxos.map(this.standardizeUtxo); const tx = new this.lib.Transaction().from(btcUtxos).feePerByte(Number(fee) + 2); if (change) { tx.change(change); diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index 641caaea12f..802e367589a 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -16,11 +16,11 @@ export class BTCTxProvider { selectCoins( recipients: Array<{ amount: number }>, - utxos: UtxoType[], + utxos: UtxoTypeE[], fee: number - ) { + ): UtxoTypeE[] { // Only sort by block height if utxos are bitcore-node style - if (this.isNodeUtxo(utxos[0])) { + if (utxos[0].mintHeight != undefined) { utxos = utxos.sort(function(a, b) { return a.mintHeight - b.mintHeight; }); @@ -38,9 +38,28 @@ export class BTCTxProvider { return filteredUtxos; } + + /** + * Standardize utxo for internal funcionality. + * Accepts either bitcore-node or lib (bitcore-lib, bitcore-lib-cash, etc.). + * Handles both lib style utxos: UnspentOutput properties and UnspentOutput.toObject properties. + * + * @param utxos either a bitcore-node or lib utxo + * @returns utxo in the standard, internaly used format + */ + standardizeUtxo(utxo: UtxoTypeE): UtxoTypeS { + return { + satoshis: utxo.satoshis ?? utxo.value ?? (utxo.amount != undefined ? this.lib.Unit.fromSatoshis(utxo.amount) : undefined), + txId: utxo.txId ?? utxo.mintTxid ?? utxo.txid, + outputIndex: utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout, + script: typeof utxo.script === 'string' ? utxo.script : utxo.script.toString() ?? utxo.scriptPubkey, + address: typeof utxo.address === 'string' ? utxo.address : utxo.address.toString() + }; + } + create(params: { recipients: Array<{ address: string; amount: number }>; - utxos: UtxoType[]; + utxos: UtxoTypeE[]; change: string; feeRate: number; fee: number; @@ -51,7 +70,7 @@ export class BTCTxProvider { }) { const { recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock } = params; const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = this.isNodeUtxo(utxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; + const btcUtxos = filteredUtxos.map(this.standardizeUtxo); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -134,15 +153,23 @@ export class BTCTxProvider { return bitcoreTx.hash; } - sign(params: { tx: string; keys: Array; utxos: UtxoType[]; pubkeys?: any[]; threshold?: number; opts: any }) { + sign(params: { + tx: string; + keys: Array; + utxos: UtxoTypeE[]; + pubkeys?: any[]; + threshold?: number; + opts: any; + }) { const { tx, keys, pubkeys, threshold, opts } = params; const utxos = params.utxos || []; const bitcoreTx = new this.lib.Transaction(tx); + const btcUtxos = utxos.map(this.standardizeUtxo); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, - utxos + utxos: btcUtxos }); - bitcoreTx.associateInputs(applicableUtxos, pubkeys, threshold, opts); + bitcoreTx.associateInputs(applicableUtxos.map(this.lib.Transaction.UnspentOutput), pubkeys, threshold, opts); const uniqePrivKeys = Object.values(keys.reduce((map, key) => { // Need to preserve (un)compressed property, so don't use key.privKey.toString(); const pk = new this.lib.PrivateKey(key.privKey); @@ -153,38 +180,13 @@ export class BTCTxProvider { return signedTx; } - /** - * Converts the utxos in a bitcore-nodes database to bitcore lib utxos - * - * @param utxos bitcore-node style utxos - * @returns lib style utxos - */ - nodeToLibUtxos(utxos: NodeUtxoType[]): BitcoreLib.Transaction.UnspentOutput[] { - return utxos.map(utxo => new this.lib.Transaction.UnspentOutput({ - satoshis: utxo.value, - // bitcore-node utxos have both mintTxid and spentTxid - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex, - script: utxo.script, - address: utxo.address - })); - } - - /** - * Return true if utxo is a bitcore-node utxo - * - * @param utxo either a bitcore-lib or bitcore-node utxo - * @returns true if node utxo - */ - isNodeUtxo(utxo: UtxoType): boolean { - return utxo.mintTxid != undefined; - } - - getRelatedUtxos(params: { outputs: any[]; utxos: UtxoType[] }) { + getRelatedUtxos(params: { + outputs: BitcoreLib.Transaction.Input[]; + utxos: UtxoTypeS[]; + }): UtxoTypeS[] { const { outputs, utxos } = params; const txids = outputs.map(output => output.toObject().prevTxId); - const applicableUtxos = utxos.filter(utxo => txids.includes(utxo.txid || utxo.mintTxid)); - return this.isNodeUtxo(utxos[0]) ? this.nodeToLibUtxos(applicableUtxos) : applicableUtxos; + return utxos.filter(utxo => txids.includes(utxo.txId)); } getOutputsFromTx({ tx }) { @@ -194,12 +196,13 @@ export class BTCTxProvider { }); } - getSigningAddresses(params: { tx: string | BitcoreLib.Transaction; utxos: UtxoType[] }): string[] { + getSigningAddresses(params: { tx: string | BitcoreLib.Transaction; utxos: UtxoTypeE[] }): string[] { const { tx, utxos } = params; const bitcoreTx = new this.lib.Transaction(tx); + const btcUtxos = utxos.map(this.standardizeUtxo); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, - utxos + utxos: btcUtxos }); return applicableUtxos.map(utxo => utxo.address); } @@ -207,7 +210,7 @@ export class BTCTxProvider { getSighash(params: { tx: string | BitcoreLib.Transaction; index: number; - utxos?: UtxoType[]; + utxos?: UtxoTypeE[]; pubKey?: string | BitcoreLib.PublicKey | BitcoreLib.HDPublicKey; path?: string; sigtype?: number; @@ -227,7 +230,8 @@ export class BTCTxProvider { tx = new this.lib.Transaction(tx); } if (utxos) { - tx.associateInputs(this.isNodeUtxo(utxos[0]) ? this.nodeToLibUtxos(utxos) : utxos, pubKeys, threshold, opts); + const btcUtxos = utxos.map(this.standardizeUtxo); + tx.associateInputs(btcUtxos.map(this.lib.Transaction.UnspentOutput), pubKeys, threshold, opts); } $.checkState(tx.inputs[index].output instanceof this.lib.Transaction.Output, 'Input must have all utxo info'); @@ -248,21 +252,27 @@ export class BTCTxProvider { } type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; -// bitcore-node style utxo minus values that are not used -type NodeUtxoType = { - // network: string; - // chain: string; + +// Standard utxo. Used internaly. +type UtxoTypeS = { + txId: string; + outputIndex: number; + satoshis: number; + address: string; + script: string; +} +// Externaly recieved utxo. Could either be node (bitcore-node) or lib (bitcore-lib, bitcore-lib-cash etc.) type. +type UtxoTypeE = UtxoTypeS & { + // node specific properties mintTxid: string; mintIndex: number; mintHeight: number; - // coinbase: boolean; value: number; - address: string; - script: string; - // spentTxid: string; - // spentHeight?: number; - // confirmations?: number; - // sequenceNumber?: number; + script: string | BitcoreLib.Address; + address: string | BitcoreLib.Script; + // UnspentOutput.toObject specific properties + txid: string; + amount: number; + vout: number; + scriptPubkey: string; } -// utxo type recieved externaly from this class that could either be from bitcore-node or already a lib utxo -type UtxoType = NodeUtxoType | BitcoreLib.Transaction.UnspentOutput; diff --git a/packages/crypto-wallet-core/src/transactions/doge/index.ts b/packages/crypto-wallet-core/src/transactions/doge/index.ts index a53c4043cfa..4b5635f3cf1 100644 --- a/packages/crypto-wallet-core/src/transactions/doge/index.ts +++ b/packages/crypto-wallet-core/src/transactions/doge/index.ts @@ -5,7 +5,7 @@ export class DOGETxProvider extends BTCTxProvider { lib = BitcoreLibDoge; create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = this.isNodeUtxo(filteredUtxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; + const btcUtxos = filteredUtxos.map(this.standardizeUtxo); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); diff --git a/packages/crypto-wallet-core/src/transactions/ltc/index.ts b/packages/crypto-wallet-core/src/transactions/ltc/index.ts index 1c28dd3fdb8..9bf55957401 100644 --- a/packages/crypto-wallet-core/src/transactions/ltc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/ltc/index.ts @@ -5,7 +5,7 @@ export class LTCTxProvider extends BTCTxProvider { lib = BitcoreLibLtc; create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = this.isNodeUtxo(filteredUtxos[0]) ? this.nodeToLibUtxos(filteredUtxos) : filteredUtxos; + const btcUtxos = filteredUtxos.map(this.standardizeUtxo); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); From c4348d79cd909fc2d8093ed4f2d4e7ba615567e0 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Mon, 10 Aug 2026 15:20:50 -0400 Subject: [PATCH 5/9] utxo CWC.Transactions: improved utxo types, multiple bug fixes, and consistent typing --- .../src/transactions/bch/index.ts | 17 ++- .../src/transactions/btc/index.ts | 137 ++++++++++-------- .../src/transactions/doge/index.ts | 17 ++- .../src/transactions/ltc/index.ts | 17 ++- 4 files changed, 116 insertions(+), 72 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/bch/index.ts b/packages/crypto-wallet-core/src/transactions/bch/index.ts index d89fca223b4..b1d7740035f 100644 --- a/packages/crypto-wallet-core/src/transactions/bch/index.ts +++ b/packages/crypto-wallet-core/src/transactions/bch/index.ts @@ -1,17 +1,24 @@ import BitcoreLibCash from '@bitpay-labs/bitcore-lib-cash'; -import { BTCTxProvider } from '../btc'; +import { BTCTxProvider, EveryUtxoType } from '../btc'; export class BCHTxProvider extends BTCTxProvider { lib = BitcoreLibCash; - create({ recipients, utxos = [], change, fee = 20000, isSweep }) { - const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(this.standardizeUtxo); + create(params: { + recipients: Array<{ address: string; amount: number }>; + utxos?: EveryUtxoType[]; + change?: string; + fee?: number | string; + isSweep?: boolean; + }): string { + const { recipients, utxos = [], change, fee = 20000, isSweep } = params; + const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos).feePerByte(Number(fee) + 2); if (change) { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } return tx.uncheckedSerialize(); } diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index 802e367589a..f279a200ea0 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -15,12 +15,12 @@ export class BTCTxProvider { lib = BitcoreLib; selectCoins( - recipients: Array<{ amount: number }>, - utxos: UtxoTypeE[], - fee: number - ): UtxoTypeE[] { + recipients: Array<{ amount: number | string }>, + utxos: EveryUtxoType[], + fee?: number + ): EveryUtxoType[] { // Only sort by block height if utxos are bitcore-node style - if (utxos[0].mintHeight != undefined) { + if (utxos.length > 0 && utxos[0].mintHeight != undefined) { utxos = utxos.sort(function(a, b) { return a.mintHeight - b.mintHeight; }); @@ -30,47 +30,47 @@ export class BTCTxProvider { let utxoSum = 0; const recepientSum = recipients.reduce((sum, cur) => sum + Number(cur.amount), fee || 0); while (utxoSum < recepientSum) { + assert(index < utxos.length, 'insufficient funds'); const utxo = utxos[index]; - utxoSum += Number(utxo.value ?? utxo.satoshis); + utxoSum += Number(utxo.value ?? utxo.satoshis ?? this.lib.Unit.fromBTC(utxo.amount).toSatoshis()); index += 1; } const filteredUtxos = utxos.slice(0, index); return filteredUtxos; } - /** * Standardize utxo for internal funcionality. - * Accepts either bitcore-node or lib (bitcore-lib, bitcore-lib-cash, etc.). + * Accepts either a bitcore-node or a lib (bitcore-lib, bitcore-lib-cash, etc.) utxo. * Handles both lib style utxos: UnspentOutput properties and UnspentOutput.toObject properties. - * + * * @param utxos either a bitcore-node or lib utxo * @returns utxo in the standard, internaly used format */ - standardizeUtxo(utxo: UtxoTypeE): UtxoTypeS { + standardizeUtxo(utxo: EveryUtxoType): UtxoType { return { - satoshis: utxo.satoshis ?? utxo.value ?? (utxo.amount != undefined ? this.lib.Unit.fromSatoshis(utxo.amount) : undefined), + satoshis: Number(utxo.satoshis ?? utxo.value ?? this.lib.Unit.fromBTC(utxo.amount ?? 0).toSatoshis()), txId: utxo.txId ?? utxo.mintTxid ?? utxo.txid, - outputIndex: utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout, - script: typeof utxo.script === 'string' ? utxo.script : utxo.script.toString() ?? utxo.scriptPubkey, - address: typeof utxo.address === 'string' ? utxo.address : utxo.address.toString() + outputIndex: Number(utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout ?? 0), + script: utxo.scriptPubKey ?? new this.lib.Script(utxo.script).toHex(), + address: utxo.address != undefined ? new this.lib.Address(utxo.address).toString() : undefined }; } create(params: { - recipients: Array<{ address: string; amount: number }>; - utxos: UtxoTypeE[]; - change: string; - feeRate: number; - fee: number; - isSweep: boolean; - replaceByFee: boolean; - lockUntilDate: number; - lockUntilBlock: number; - }) { + recipients: Array<{ address: string; amount: number | string }>; + utxos?: EveryUtxoType[]; + change?: string; + feeRate?: number | string; + fee?: number | string; + isSweep?: boolean; + replaceByFee?: boolean; + lockUntilDate?: number; + lockUntilBlock?: number; + }): string { const { recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock } = params; - const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(this.standardizeUtxo); + const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -82,7 +82,7 @@ export class BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount as any)); + tx.to(recipient.address, Number(recipient.amount)); } if (replaceByFee && typeof tx.enableRBF === 'function') { tx.enableRBF(); @@ -99,7 +99,7 @@ export class BTCTxProvider { throw new Error('function getSignature not implemented for UTXO coins'); } - transformSignatureObject(params: { obj: any; sigtype?: number }) { + transformSignatureObject(params: { obj: any; sigtype?: number }): string { const { obj, sigtype } = params; const { v } = obj; let { r, s, i, nhashtype } = obj; @@ -127,7 +127,12 @@ export class BTCTxProvider { return new this.lib.crypto.Signature({ r, s, i, nhashtype }).toString(); } - applySignature(params: { tx: BitcoreLib.Transaction; signature: SignatureType; index: number; sigtype?: number }) { + applySignature(params: { + tx: BitcoreLib.Transaction; + signature: SignatureType; + index: number; + sigtype?: number; + }): BitcoreLib.Transaction { const { index, sigtype, tx } = params; let { signature } = params; assert(tx instanceof this.lib.Transaction, 'tx must be an instance of Transaction'); @@ -148,28 +153,28 @@ export class BTCTxProvider { return tx; } - getHash(params: { tx: string }) { + getHash(params: { tx: TransactionType }): string { const bitcoreTx = new this.lib.Transaction(params.tx); return bitcoreTx.hash; } sign(params: { - tx: string; - keys: Array; - utxos: UtxoTypeE[]; + tx: TransactionType; + keys: Key[]; + utxos: EveryUtxoType[]; pubkeys?: any[]; threshold?: number; opts: any; - }) { + }): string { const { tx, keys, pubkeys, threshold, opts } = params; const utxos = params.utxos || []; const bitcoreTx = new this.lib.Transaction(tx); - const btcUtxos = utxos.map(this.standardizeUtxo); + const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, utxos: btcUtxos }); - bitcoreTx.associateInputs(applicableUtxos.map(this.lib.Transaction.UnspentOutput), pubkeys, threshold, opts); + bitcoreTx.associateInputs(applicableUtxos.map(utxo => new this.lib.Transaction.UnspentOutput(utxo)), pubkeys, threshold, opts); const uniqePrivKeys = Object.values(keys.reduce((map, key) => { // Need to preserve (un)compressed property, so don't use key.privKey.toString(); const pk = new this.lib.PrivateKey(key.privKey); @@ -182,24 +187,29 @@ export class BTCTxProvider { getRelatedUtxos(params: { outputs: BitcoreLib.Transaction.Input[]; - utxos: UtxoTypeS[]; - }): UtxoTypeS[] { + utxos: UtxoType[]; + }): UtxoType[] { const { outputs, utxos } = params; const txids = outputs.map(output => output.toObject().prevTxId); return utxos.filter(utxo => txids.includes(utxo.txId)); } - getOutputsFromTx({ tx }) { - return tx.outputs.map(({ script, satoshis }) => { + getOutputsFromTx(params: { + tx: BitcoreLib.Transaction; + }): Array<{ address: string | BitcoreLib.Script; satoshis: number }> { + return params.tx.outputs.map(({ script, satoshis }) => { const address = script; return { address, satoshis }; }); } - getSigningAddresses(params: { tx: string | BitcoreLib.Transaction; utxos: UtxoTypeE[] }): string[] { + getSigningAddresses(params: { + tx: TransactionType; + utxos: EveryUtxoType[]; + }): (string | undefined)[] { const { tx, utxos } = params; const bitcoreTx = new this.lib.Transaction(tx); - const btcUtxos = utxos.map(this.standardizeUtxo); + const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, utxos: btcUtxos @@ -208,9 +218,9 @@ export class BTCTxProvider { } getSighash(params: { - tx: string | BitcoreLib.Transaction; + tx: TransactionType; index: number; - utxos?: UtxoTypeE[]; + utxos?: EveryUtxoType[]; pubKey?: string | BitcoreLib.PublicKey | BitcoreLib.HDPublicKey; path?: string; sigtype?: number; @@ -230,8 +240,8 @@ export class BTCTxProvider { tx = new this.lib.Transaction(tx); } if (utxos) { - const btcUtxos = utxos.map(this.standardizeUtxo); - tx.associateInputs(btcUtxos.map(this.lib.Transaction.UnspentOutput), pubKeys, threshold, opts); + const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); + tx.associateInputs(btcUtxos.map(utxo => new this.lib.Transaction.UnspentOutput(utxo)), pubKeys, threshold, opts); } $.checkState(tx.inputs[index].output instanceof this.lib.Transaction.Output, 'Input must have all utxo info'); @@ -253,26 +263,39 @@ export class BTCTxProvider { type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; -// Standard utxo. Used internaly. -type UtxoTypeS = { +/** Transaction data that can be converted into a Transaction via Transaction(tx) */ +type TransactionType = BitcoreLib.Transaction | string | Buffer | object; + +/** + * Standard utxo type use for internal processing. + * Property names are from bitcore-lib's UnspentOutput. + * Note, UnspentOutput addresses and scripts are Address and Script classes respectively, + * here they are both strings. + */ +export type UtxoType = { txId: string; outputIndex: number; satoshis: number; - address: string; script: string; -} -// Externaly recieved utxo. Could either be node (bitcore-node) or lib (bitcore-lib, bitcore-lib-cash etc.) type. -type UtxoTypeE = UtxoTypeS & { - // node specific properties + address?: string; +}; + +/** + * Utxo type for functions were the received utxo type is unknown. + * Could either be in the format of UnspentOutput, UnspentOutput.toObject, or from bitcore-node. + */ +export type EveryUtxoType = Partial; diff --git a/packages/crypto-wallet-core/src/transactions/doge/index.ts b/packages/crypto-wallet-core/src/transactions/doge/index.ts index 4b5635f3cf1..b8e38d9de8a 100644 --- a/packages/crypto-wallet-core/src/transactions/doge/index.ts +++ b/packages/crypto-wallet-core/src/transactions/doge/index.ts @@ -1,11 +1,18 @@ import BitcoreLibDoge from '@bitpay-labs/bitcore-lib-doge'; -import { BTCTxProvider } from '../btc'; +import { BTCTxProvider, EveryUtxoType } from '../btc'; export class DOGETxProvider extends BTCTxProvider { lib = BitcoreLibDoge; - create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { - const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(this.standardizeUtxo); + create(params: { + recipients: Array<{ address: string; amount: number }>; + utxos?: EveryUtxoType[]; + change?: string; + feeRate?: number | string; + fee?: number | string; + }): string { + const { recipients, utxos = [], change, feeRate, fee = 20000 } = params; + const filteredUtxos = this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -17,7 +24,7 @@ export class DOGETxProvider extends BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } return tx.uncheckedSerialize(); } diff --git a/packages/crypto-wallet-core/src/transactions/ltc/index.ts b/packages/crypto-wallet-core/src/transactions/ltc/index.ts index 9bf55957401..2fdc4840592 100644 --- a/packages/crypto-wallet-core/src/transactions/ltc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/ltc/index.ts @@ -1,11 +1,18 @@ import BitcoreLibLtc from '@bitpay-labs/bitcore-lib-ltc'; -import { BTCTxProvider } from '../btc'; +import { BTCTxProvider, EveryUtxoType } from '../btc'; export class LTCTxProvider extends BTCTxProvider { lib = BitcoreLibLtc; - create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { - const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(this.standardizeUtxo); + create(params: { + recipients: Array<{ address: string; amount: number }>; + utxos?: EveryUtxoType[]; + change?: string; + feeRate?: number | string; + fee?: number | string; + }): string { + const { recipients, utxos = [], change, feeRate, fee = 20000 } = params; + const filteredUtxos = this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -17,7 +24,7 @@ export class LTCTxProvider extends BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } return tx.uncheckedSerialize(); } From f408d3d9a046d1482e8f1fd1f1a788b1a0eec1e0 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Thu, 13 Aug 2026 15:34:26 -0400 Subject: [PATCH 6/9] created tests for all utxo types, all but wip singing --- .../test/transactions.test.ts | 171 +++++++++++++++++- 1 file changed, 169 insertions(+), 2 deletions(-) diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index 52f12d405d8..2bdecb49a7f 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -6,7 +6,13 @@ import bitcoreLibDoge from '@bitpay-labs/bitcore-lib-doge'; import bitcoreLibLtc from '@bitpay-labs/bitcore-lib-ltc'; import { Constants, Transactions } from '../src'; -describe('Transaction', function() { +describe('Transaction', function () { + const libs = { + BTC: bitcoreLib, + BCH: bitcoreLibCash, + DOGE: bitcoreLibDoge, + LTC: bitcoreLibLtc + }; describe('create', () => { it('should create a BTC tx', () => { const recipients = [{ address: 'mpNpzMoprLnSBu8CWDunNCYeJq3Mzdk59V', amount: 1e8 }]; @@ -45,6 +51,167 @@ describe('Transaction', function() { expect(signed).to.eq(expected); }); + describe.only('every utxo type: bitcore-node, UnspentOutput, and UnspentOutput.toObject', () => { + const keys = [{ + address: '15GBbJcKKKcXt9fMx4drHvb2GLWMksEvAq', + privKey: '37ffacfa88637b5b1835e44e2976a92e883b5480bde433f42735d1d1943270df' + }]; + + const bitcoreNodeUtxos = + [ + { + mintTxid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + mintIndex: 1, + value: 90_000, + script: '76a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac', + address: 'moVnNJpHHfssYJEnMTS5xXyGV8RhRQNRz5', + sequenceNumber: 4294967294 + }, + { + mintTxid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + mintIndex: 0, + value: 30_000, + script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac', + address: 'mnfnJx2xWWptYmBzck3rdE851Dtu9GaZ3F', + sequenceNumber: 4294967294 + } + ]; + const unspentOutputUtxos = [ + { + txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + outputIndex: 1, + satoshis: 90_000, + script: '76a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac' + }, + { + txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + outputIndex: 0, + satoshis: 30_000, + script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' + } + ]; + const unspentOutputToObjectUtxos = [ + { + txid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + outputIndex: 1, + amount: 0.0009, + scriptPubKey: '76a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac' + }, + { + txid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + vout: 0, + amount: 0.0003, + scriptPubKey: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' + } + ]; + const utxoSet = [ + bitcoreNodeUtxos, + unspentOutputUtxos, + unspentOutputToObjectUtxos + ]; + + const recipients = [{ address: 'moVnNJpHHfssYJEnMTS5xXyGV8RhRQNRz5', amount: 100_000 }]; + for (const chain of ['BTC', 'BCH', 'DOGE', 'LTC']) { + let tx: string; + const lib = libs[chain]; + it(`should create a tx with every utxo type for ${chain}`, () => { + const txs = utxoSet.map(utxos => Transactions.create({ + chain, + recipients, + utxos + })); + + tx = txs[0]; + for (const _tx of txs.slice(1)) { + expect(tx, 'all transactions should be the same regardless of the utxo format').to.equal(_tx); + } + + let expectedTx: string; + if (chain === 'DOGE') { + expectedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640000000000ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + } else { + expectedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640000000000ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + } + expect(tx).to.equal(expectedTx); + }); + + it(`should sign a tx with every utxo type ${chain}`, () => { + const signedTxs = utxoSet.map(utxos => Transactions.sign({ + chain, + tx, + utxos, + keys + })); + const signedTx = signedTxs[0]; + for (const _signedTx of signedTxs.slice(1)) { + expect(signedTx, 'signed transactions should all be the same regardless of utxo type').to.equal(_signedTx); + } + + let expectedTx: string; + if (chain === 'DOGE') { + expectedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640000000000ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + } else { + expectedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640000000000ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + } + expect(signedTx).to.equal(expectedTx); + }); + + it(`should create valid sighashes for all utxo types ${chain}`, () => { + const bitcoreTx = lib.Transaction() + .from(unspentOutputUtxos) + .to(recipients[0].address, recipients[0].amount); + const signedTx = lib.Transaction() + .from(unspentOutputUtxos) + .to(recipients[0].address, recipients[0].amount); + + for (let index = 0; index < unspentOutputUtxos.length; index++) { + const sighashes: string[] = []; + for (const utxos of utxoSet) { + sighashes.push(Transactions.getSighash({ + chain, + tx, + utxos, + index, + sigtype: lib.crypto.Signature.SIGHASH_ALL + })); + } + + // getSighash should work without utxos if a complete lib transaction is provided + sighashes.push(Transactions.getSighash({ + chain, + tx: bitcoreTx, + index, + sigtype: lib.crypto.Signature.SIGHASH_ALL + })); + + const sighash = sighashes[0]; + for (const hash of sighashes) { + expect(sighash).to.equal(hash); + } + expect(sighash.length).to.equal(64); + const privateKey = new lib.PrivateKey(keys[0].privKey); + const publicKey = privateKey.toPublicKey(); + const signature = lib.crypto.ECDSA.sign(Buffer.from(sighash, 'hex'), privateKey); + signature.pubKey = publicKey; + + Transactions.applySignature({ + chain, + tx: signedTx, + signature, + index + }); + } + if (chain === 'DOGE') { + const serializedSignedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006b483045022100a2399f85c809a1f8dbc0a5c38c80651330e1c48f744915c7692b36436a2cfd2302200daf1fa95dde4d16faaf0b2c6fd35998d905b868da73e2cb9d071bb1143e96d40121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b48304502210098e92c8adf6240c7550adb527d76498d18d565ab0ad8735e2d7405cb2bfd26a1022054eb13e7b3d5bea3ad7298698e3e740293a009c11a2ba37c607a50b8be4706cd0121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + expect(signedTx.serialize({ disableSmallFees: true, disableDustOutputs: true })).to.equal(serializedSignedTx); + } else { + const serializedSignedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006a47304402207442da2e4ad78e5527c401ac9af350678fb1268bc060a873836eaa30577085a10220692b49e134be6f4c7edd8a0a237f6a0826853f18b2902953a50f3838d2cb76330121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b483045022100d03db3157a7fe45f0e3240c693b8bdc041a48e1869b9d91858c38288def30cd802206c5c3679adb66cbe5c32dd0ea9d80c798e69167e78423f99a9158138076a2bbc0121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + expect(signedTx.serialize()).to.equal(serializedSignedTx); + } + }); + } + }); + it('should sign a BTC opreturn tx', () => { const tx = '0200000001ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffff0200000000000000000b6a096a07696f6e3a61626340420f00000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; @@ -1887,4 +2054,4 @@ describe('Transaction', function() { }); }); -}); \ No newline at end of file +}); From 1ec8cd115f9b17075611a55dd5dd0b6c357f41b8 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 14 Aug 2026 09:40:52 -0400 Subject: [PATCH 7/9] fixed test signing and added sigtype to Transactions.sign --- .../src/transactions/btc/index.ts | 5 ++-- .../test/transactions.test.ts | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index f279a200ea0..7a8492392ed 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -162,11 +162,12 @@ export class BTCTxProvider { tx: TransactionType; keys: Key[]; utxos: EveryUtxoType[]; + sigtype?: number; pubkeys?: any[]; threshold?: number; opts: any; }): string { - const { tx, keys, pubkeys, threshold, opts } = params; + const { tx, keys, pubkeys, sigtype, threshold, opts } = params; const utxos = params.utxos || []; const bitcoreTx = new this.lib.Transaction(tx); const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); @@ -181,7 +182,7 @@ export class BTCTxProvider { map[pk.publicKey.toString()] = pk; return map; }, {})); - const signedTx = bitcoreTx.sign(uniqePrivKeys).toString(); + const signedTx = bitcoreTx.sign(uniqePrivKeys, sigtype).toString(); return signedTx; } diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index 2bdecb49a7f..e5567b3ae1d 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -53,8 +53,8 @@ describe('Transaction', function () { describe.only('every utxo type: bitcore-node, UnspentOutput, and UnspentOutput.toObject', () => { const keys = [{ - address: '15GBbJcKKKcXt9fMx4drHvb2GLWMksEvAq', - privKey: '37ffacfa88637b5b1835e44e2976a92e883b5480bde433f42735d1d1943270df' + address: 'mnfnJx2xWWptYmBzck3rdE851Dtu9GaZ3F', + privKey: 'cSFjiifSbZ2hU4jTFwE993LCe2rkZGULCTGWTDWXzHvuXRKxpnc1' }]; const bitcoreNodeUtxos = @@ -63,8 +63,8 @@ describe('Transaction', function () { mintTxid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', mintIndex: 1, value: 90_000, - script: '76a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac', - address: 'moVnNJpHHfssYJEnMTS5xXyGV8RhRQNRz5', + script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac', + address: 'mnfnJx2xWWptYmBzck3rdE851Dtu9GaZ3F', sequenceNumber: 4294967294 }, { @@ -81,7 +81,7 @@ describe('Transaction', function () { txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', outputIndex: 1, satoshis: 90_000, - script: '76a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac' + script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' }, { txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', @@ -95,7 +95,7 @@ describe('Transaction', function () { txid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', outputIndex: 1, amount: 0.0009, - scriptPubKey: '76a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac' + scriptPubKey: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' }, { txid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', @@ -140,7 +140,8 @@ describe('Transaction', function () { chain, tx, utxos, - keys + keys, + sigtype: lib.crypto.Signature.SIGHASH_ALL })); const signedTx = signedTxs[0]; for (const _signedTx of signedTxs.slice(1)) { @@ -149,9 +150,9 @@ describe('Transaction', function () { let expectedTx: string; if (chain === 'DOGE') { - expectedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640000000000ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + expectedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006a473044022015ceee1da23792e26cd1215c8327a35978c8b0494de9e69bab6d31ab3d0b56ad02205f9299162591672c6cde3d92d5819a11e1ad9f353a25d7a3ce494877ef27468901210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b4830450221009f0b5dd0b4c0bd9bcf804e71020705eb9bda99a31cb2a4d123c364bdd394adeb022016939a51fb03c9efea4b7d53a7b90d7a3667fff71cd8365e366c81b1fdd0d3e001210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; } else { - expectedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640100000000ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e640000000000ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + expectedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006b483045022100c74ab4fe359e8efc73fc28dce989e28da47d3b45eb06ee305dfc0e675da8700802201076a067337a3c6b90fad2077a55154aa150460cb842bc86373026eb071a7ac901210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b483045022100ed22907cc96b5367ef469225f7f718efa8fb4eb2dc5fbf3909777c5f024cf5e902206480082ad9b413a8a76128ab147287da30d907cfb1a159be814392fa1138012801210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; } expect(signedTx).to.equal(expectedTx); }); @@ -202,10 +203,10 @@ describe('Transaction', function () { }); } if (chain === 'DOGE') { - const serializedSignedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006b483045022100a2399f85c809a1f8dbc0a5c38c80651330e1c48f744915c7692b36436a2cfd2302200daf1fa95dde4d16faaf0b2c6fd35998d905b868da73e2cb9d071bb1143e96d40121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b48304502210098e92c8adf6240c7550adb527d76498d18d565ab0ad8735e2d7405cb2bfd26a1022054eb13e7b3d5bea3ad7298698e3e740293a009c11a2ba37c607a50b8be4706cd0121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + const serializedSignedTx = '0100000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006a473044022015ceee1da23792e26cd1215c8327a35978c8b0494de9e69bab6d31ab3d0b56ad02205f9299162591672c6cde3d92d5819a11e1ad9f353a25d7a3ce494877ef27468901210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b4830450221009f0b5dd0b4c0bd9bcf804e71020705eb9bda99a31cb2a4d123c364bdd394adeb022016939a51fb03c9efea4b7d53a7b90d7a3667fff71cd8365e366c81b1fdd0d3e001210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; expect(signedTx.serialize({ disableSmallFees: true, disableDustOutputs: true })).to.equal(serializedSignedTx); } else { - const serializedSignedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006a47304402207442da2e4ad78e5527c401ac9af350678fb1268bc060a873836eaa30577085a10220692b49e134be6f4c7edd8a0a237f6a0826853f18b2902953a50f3838d2cb76330121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b483045022100d03db3157a7fe45f0e3240c693b8bdc041a48e1869b9d91858c38288def30cd802206c5c3679adb66cbe5c32dd0ea9d80c798e69167e78423f99a9158138076a2bbc0121022df004d5108312f62ae085913b1d029007d980ed61f04816ae9ccfc404c99e2cffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; + const serializedSignedTx = '0200000002ab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64010000006b483045022100c74ab4fe359e8efc73fc28dce989e28da47d3b45eb06ee305dfc0e675da8700802201076a067337a3c6b90fad2077a55154aa150460cb842bc86373026eb071a7ac901210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffffab189f0d3bf494d3307effb79bafe0758907b86621edb8bd4cad4c6c6dc63e64000000006b483045022100ed22907cc96b5367ef469225f7f718efa8fb4eb2dc5fbf3909777c5f024cf5e902206480082ad9b413a8a76128ab147287da30d907cfb1a159be814392fa1138012801210321f2f13aed42db7257b64f77d574071a6e81e460ab3693eefb7482c12d1ff697ffffffff01a0860100000000001976a91457884dcfe2ab46d3354a42d97333c95e5b80cf0188ac00000000'; expect(signedTx.serialize()).to.equal(serializedSignedTx); } }); From 5dd5c27a864770f9162585575db25463fbe891cf Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 14 Aug 2026 09:49:50 -0400 Subject: [PATCH 8/9] removed .only from CWC test --- packages/crypto-wallet-core/test/transactions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index e5567b3ae1d..d8cfc671642 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -51,7 +51,7 @@ describe('Transaction', function () { expect(signed).to.eq(expected); }); - describe.only('every utxo type: bitcore-node, UnspentOutput, and UnspentOutput.toObject', () => { + describe('every utxo type: bitcore-node, UnspentOutput, and UnspentOutput.toObject', () => { const keys = [{ address: 'mnfnJx2xWWptYmBzck3rdE851Dtu9GaZ3F', privKey: 'cSFjiifSbZ2hU4jTFwE993LCe2rkZGULCTGWTDWXzHvuXRKxpnc1' From eddacdbc3ff6994d92d487ac98635f7acb1b77fd Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 14 Aug 2026 10:00:27 -0400 Subject: [PATCH 9/9] added mintHeight to bitcore-node utxos for CWC.Transactions tests --- packages/crypto-wallet-core/test/transactions.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index d8cfc671642..d6bf78d8d26 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -62,6 +62,7 @@ describe('Transaction', function () { { mintTxid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', mintIndex: 1, + mintHeight: 100, value: 90_000, script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac', address: 'mnfnJx2xWWptYmBzck3rdE851Dtu9GaZ3F', @@ -70,6 +71,7 @@ describe('Transaction', function () { { mintTxid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', mintIndex: 0, + mintHeight: 100, value: 30_000, script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac', address: 'mnfnJx2xWWptYmBzck3rdE851Dtu9GaZ3F',