From 9a8a6b58b5486cf07a27b19ffa54a70dcb8db541 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Thu, 6 Aug 2026 23:33:46 -0400 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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', From b5c636b23a0284e4ae867a06fd8b1efdb2d8d8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=9B=E3=83=83=E3=83=88=E3=83=94=E3=82=B0?= <93409262+MicahMaphet@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:03:43 -0400 Subject: [PATCH 10/14] Apply batched suggestions from code review crypto-wallet-core handle empty ("") address for utxo conversion Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/crypto-wallet-core/src/transactions/btc/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index 7a8492392ed..c7502315e69 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -40,7 +40,7 @@ export class BTCTxProvider { } /** - * Standardize utxo for internal funcionality. + * Standardize utxo for internal functionality. * 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. * @@ -53,7 +53,7 @@ export class BTCTxProvider { txId: utxo.txId ?? utxo.mintTxid ?? utxo.txid, 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 + address: utxo.address ? new this.lib.Address(utxo.address).toString() : undefined }; } From f5607c71eb8e17ffb287a079ba54c299215636f9 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Thu, 10 Sep 2026 11:10:41 -0400 Subject: [PATCH 11/14] cwc test UnspentOutput rather than just the fields it has --- packages/crypto-wallet-core/test/transactions.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index d6bf78d8d26..68fa463649c 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -79,19 +79,20 @@ describe('Transaction', function () { } ]; const unspentOutputUtxos = [ - { + new bitcoreLib.Transaction.UnspentOutput({ txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', outputIndex: 1, satoshis: 90_000, script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' - }, - { + }), + new bitcoreLib.Transaction.UnspentOutput({ txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', outputIndex: 0, satoshis: 30_000, script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' - } + }) ]; + const unspentOutputToObjectUtxos = [ { txid: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', From 4a42b26563736d1d81b1e3fa6887c385bdafdaf5 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 11 Sep 2026 14:15:52 -0400 Subject: [PATCH 12/14] fix bug in bitcore-lib-cash interpreter --- .../lib/script/interpreter.js | 1174 ++++++++--------- 1 file changed, 586 insertions(+), 588 deletions(-) diff --git a/packages/bitcore-lib-cash/lib/script/interpreter.js b/packages/bitcore-lib-cash/lib/script/interpreter.js index bce9347f9d2..08af13c71b3 100644 --- a/packages/bitcore-lib-cash/lib/script/interpreter.js +++ b/packages/bitcore-lib-cash/lib/script/interpreter.js @@ -1,16 +1,15 @@ 'use strict'; var _ = require('lodash'); - -var Script = require('./script'); -var Opcode = require('../opcode'); var BN = require('../crypto/bn'); -var Hash = require('../crypto/hash'); -var Signature = require('../crypto/signature'); -var PublicKey = require('../publickey'); var ECDSA = require('../crypto/ecdsa'); +var Hash = require('../crypto/hash'); var Schnorr = require('../crypto/schnorr'); +var Signature = require('../crypto/signature'); var BufferWriter = require('../encoding/bufferwriter'); +var Opcode = require('../opcode'); +var PublicKey = require('../publickey'); +var Script = require('./script'); @@ -51,7 +50,7 @@ var Interpreter = function Interpreter(obj) { * Translated from bitcoind's VerifyScript */ Interpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags, satoshisBN) { - var Transaction = require('../transaction'); + const Transaction = require('../transaction'); this.nSigChecks = 0; @@ -86,7 +85,7 @@ Interpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags, flags: flags, satoshisBN: satoshisBN, }); - var stackCopy; + let stackCopy; if ((flags & Interpreter.SCRIPT_VERIFY_SIGPUSHONLY) !== 0 && !scriptSig.isPushOnly()) { this.errstr = 'SCRIPT_ERR_SIG_PUSHONLY'; @@ -102,7 +101,7 @@ Interpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags, stackCopy = this.stack.slice(); } - var stack = this.stack; + const stack = this.stack; this.initialize(); this.set({ script: scriptPubkey, @@ -123,7 +122,7 @@ Interpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags, return false; } - var buf = this.stack[this.stack.length - 1]; + const buf = this.stack[this.stack.length - 1]; if (!Interpreter.castToBool(buf)) { this.errstr = 'SCRIPT_ERR_EVAL_FALSE_IN_STACK'; return false; @@ -145,8 +144,8 @@ Interpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags, throw new Error('internal error - stack copy empty'); } - var redeemScriptSerialized = stackCopy[stackCopy.length - 1]; - var redeemScript = Script.fromBuffer(redeemScriptSerialized); + const redeemScriptSerialized = stackCopy[stackCopy.length - 1]; + const redeemScript = Script.fromBuffer(redeemScriptSerialized); stackCopy.pop(); this.initialize(); @@ -186,17 +185,17 @@ Interpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags, // a clean stack (the P2SH inputs remain). The same holds for witness // evaluation. if ((flags & Interpreter.SCRIPT_VERIFY_CLEANSTACK) != 0) { - // Disallow CLEANSTACK without P2SH, as otherwise a switch - // CLEANSTACK->P2SH+CLEANSTACK would be possible, which is not a - // softfork (and P2SH should be one). - if ((flags & Interpreter.SCRIPT_VERIFY_P2SH) == 0) { - throw new Error('internal error - CLEANSTACK without P2SH'); - } + // Disallow CLEANSTACK without P2SH, as otherwise a switch + // CLEANSTACK->P2SH+CLEANSTACK would be possible, which is not a + // softfork (and P2SH should be one). + if ((flags & Interpreter.SCRIPT_VERIFY_P2SH) == 0) { + throw new Error('internal error - CLEANSTACK without P2SH'); + } - if (this.stack.length != 1) { - this.errstr = 'SCRIPT_ERR_CLEANSTACK'; - return false; - } + if (this.stack.length != 1) { + this.errstr = 'SCRIPT_ERR_CLEANSTACK'; + return false; + } } if (flags & Interpreter.SCRIPT_VERIFY_INPUT_SIGCHECKS) { @@ -398,7 +397,7 @@ Interpreter.SCRIPT_ENABLE_P2SH_32 = (1 << 26); Interpreter.SCRIPT_ENABLE_TOKENS = (1 << 27); Interpreter.castToBool = function(buf) { - for (var i = 0; i < buf.length; i++) { + for (let i = 0; i < buf.length; i++) { if (buf[i] !== 0) { // can be negative zero if (i === buf.length - 1 && buf[i] === 0x80) { @@ -412,16 +411,16 @@ Interpreter.castToBool = function(buf) { Interpreter.isSchnorrSig = function(buf) { return (buf.length === 64 || buf.length === 65) && (buf[0] !== 0x30); -} +}; /** * Translated from bitcoind's CheckSignatureEncoding */ Interpreter.prototype.checkRawSignatureEncoding = function(buf) { - var sig; + let sig; - //TODO update interpreter.js and necessary functions to match bitcoin-abc interpreter.cpp - if(Interpreter.isSchnorrSig(buf)) { + // TODO update interpreter.js and necessary functions to match bitcoin-abc interpreter.cpp + if (Interpreter.isSchnorrSig(buf)) { return true; } @@ -443,20 +442,20 @@ Interpreter.prototype.checkRawSignatureEncoding = function(buf) { // Back compat Interpreter.prototype.checkSignatureEncoding = -Interpreter.prototype.checkTxSignatureEncoding = function(buf) { + Interpreter.prototype.checkTxSignatureEncoding = function(buf) { // Empty signature. Not strictly DER encoded, but allowed to provide a // compact way to provide an invalid signature for use with CHECK(MULTI)SIG if (buf.length == 0) { - return true; + return true; } - if (!this.checkRawSignatureEncoding(buf.slice(0,buf.length-1))) { + if (!this.checkRawSignatureEncoding(buf.slice(0, buf.length-1))) { return false; } if ((this.flags & Interpreter.SCRIPT_VERIFY_STRICTENC) !== 0) { - var sig = Signature.fromTxFormat(buf); + const sig = Signature.fromTxFormat(buf); if (!sig.hasDefinedHashtype()) { this.errstr = 'SCRIPT_ERR_SIG_HASHTYPE'; return false; @@ -475,16 +474,16 @@ Interpreter.prototype.checkTxSignatureEncoding = function(buf) { } return true; -}; + }; Interpreter.prototype.checkDataSignatureEncoding = function(buf) { - // Empty signature. Not strictly DER encoded, but allowed to provide a - // compact way to provide an invalid signature for use with CHECK(MULTI)SIG - if (buf.length == 0) { - return true; - } + // Empty signature. Not strictly DER encoded, but allowed to provide a + // compact way to provide an invalid signature for use with CHECK(MULTI)SIG + if (buf.length == 0) { + return true; + } - return this.checkRawSignatureEncoding(buf); + return this.checkRawSignatureEncoding(buf); }; @@ -502,7 +501,7 @@ Interpreter.prototype.checkPubkeyEncoding = function(buf) { }; function IsCompressedOrUncompressedPubkey(bufPubkey) { - switch(bufPubkey.length) { + switch (bufPubkey.length) { case 33: return bufPubkey[0] === 0x02 || bufPubkey[0] === 0x03; case 64: @@ -523,27 +522,27 @@ function IsCompressedOrUncompressedPubkey(bufPubkey) { Interpreter._isMinimallyEncoded = function(buf, nMaxNumSize) { nMaxNumSize = nMaxNumSize || Interpreter.MAXIMUM_ELEMENT_SIZE; - if (buf.length > nMaxNumSize ) { - return false; + if (buf.length > nMaxNumSize ) { + return false; } if (buf.length > 0) { - // Check that the number is encoded with the minimum possible number - // of bytes. - // - // If the most-significant-byte - excluding the sign bit - is zero - // then we're not minimal. Note how this test also rejects the - // negative-zero encoding, 0x80. - if ((buf[buf.length-1] & 0x7f) == 0) { - // One exception: if there's more than one byte and the most - // significant bit of the second-most-significant-byte is set it - // would conflict with the sign bit. An example of this case is - // +-255, which encode to 0xff00 and 0xff80 respectively. - // (big-endian). - if (buf.length <= 1 || (buf[buf.length - 2] & 0x80) == 0) { - return false; - } + // Check that the number is encoded with the minimum possible number + // of bytes. + // + // If the most-significant-byte - excluding the sign bit - is zero + // then we're not minimal. Note how this test also rejects the + // negative-zero encoding, 0x80. + if ((buf[buf.length-1] & 0x7f) == 0) { + // One exception: if there's more than one byte and the most + // significant bit of the second-most-significant-byte is set it + // would conflict with the sign bit. An example of this case is + // +-255, which encode to 0xff00 and 0xff80 respectively. + // (big-endian). + if (buf.length <= 1 || (buf[buf.length - 2] & 0x80) == 0) { + return false; } + } } return true; }; @@ -555,47 +554,47 @@ Interpreter._isMinimallyEncoded = function(buf, nMaxNumSize) { * @param {number} nMaxNumSize (max allowed size) */ Interpreter._minimallyEncode = function(buf) { - if (buf.length == 0) { - return buf; - } + if (buf.length == 0) { + return buf; + } - // If the last byte is not 0x00 or 0x80, we are minimally encoded. - var last = buf[buf.length - 1]; - if (last & 0x7f) { - return buf; - } + // If the last byte is not 0x00 or 0x80, we are minimally encoded. + const last = buf[buf.length - 1]; + if (last & 0x7f) { + return buf; + } - // If the script is one byte long, then we have a zero, which encodes as an - // empty array. - if (buf.length == 1) { - return Buffer.from(''); - } + // If the script is one byte long, then we have a zero, which encodes as an + // empty array. + if (buf.length == 1) { + return Buffer.from(''); + } - // If the next byte has it sign bit set, then we are minimaly encoded. - if (buf[buf.length - 2] & 0x80) { - return buf; - } + // If the next byte has it sign bit set, then we are minimaly encoded. + if (buf[buf.length - 2] & 0x80) { + return buf; + } - // We are not minimally encoded, we need to figure out how much to trim. - for (var i = buf.length - 1; i > 0; i--) { - // We found a non zero byte, time to encode. - if (buf[i - 1] != 0) { - if (buf[i - 1] & 0x80) { - // We found a byte with it sign bit set so we need one more - // byte. - buf[i++] = last; - } else { - // the sign bit is clear, we can use it. - buf[i - 1] |= last; - } + // We are not minimally encoded, we need to figure out how much to trim. + for (let i = buf.length - 1; i > 0; i--) { + // We found a non zero byte, time to encode. + if (buf[i - 1] != 0) { + if (buf[i - 1] & 0x80) { + // We found a byte with it sign bit set so we need one more + // byte. + buf[i++] = last; + } else { + // the sign bit is clear, we can use it. + buf[i - 1] |= last; + } - return buf.slice(0,i); - } + return buf.slice(0, i); } + } - // If we the whole thing is zeros, then we have a zero. - return Buffer.from(''); -} + // If we the whole thing is zeros, then we have a zero. + return Buffer.from(''); +}; @@ -612,7 +611,7 @@ Interpreter.prototype.evaluate = function() { try { while (this.pc < this.script.chunks.length) { - var fSuccess = this.step(); + const fSuccess = this.step(); if (!fSuccess) { return false; } @@ -654,7 +653,7 @@ Interpreter.prototype.checkLockTime = function(nLockTime) { // unless the type of nLockTime being tested is the same as // the nLockTime in the transaction. if (!( - (this.tx.nLockTime < Interpreter.LOCKTIME_THRESHOLD && nLockTime.lt(Interpreter.LOCKTIME_THRESHOLD_BN)) || + (this.tx.nLockTime < Interpreter.LOCKTIME_THRESHOLD && nLockTime.lt(Interpreter.LOCKTIME_THRESHOLD_BN)) || (this.tx.nLockTime >= Interpreter.LOCKTIME_THRESHOLD && nLockTime.gte(Interpreter.LOCKTIME_THRESHOLD_BN)) )) { return false; @@ -681,7 +680,7 @@ Interpreter.prototype.checkLockTime = function(nLockTime) { } return true; -} +}; /** @@ -692,54 +691,54 @@ Interpreter.prototype.checkLockTime = function(nLockTime) { */ Interpreter.prototype.checkSequence = function(nSequence) { - // Relative lock times are supported by comparing the passed in operand to - // the sequence number of the input. - var txToSequence = this.tx.inputs[this.nin].sequenceNumber; + // Relative lock times are supported by comparing the passed in operand to + // the sequence number of the input. + const txToSequence = this.tx.inputs[this.nin].sequenceNumber; - // Fail if the transaction's version number is not set high enough to - // trigger BIP 68 rules. - if (this.tx.version < 2) { - return false; - } + // Fail if the transaction's version number is not set high enough to + // trigger BIP 68 rules. + if (this.tx.version < 2) { + return false; + } - // Sequence numbers with their most significant bit set are not consensus - // constrained. Testing that the transaction's sequence number do not have - // this bit set prevents using this property to get around a - // CHECKSEQUENCEVERIFY check. - if (txToSequence & SEQUENCE_LOCKTIME_DISABLE_FLAG) { - return false; - } + // Sequence numbers with their most significant bit set are not consensus + // constrained. Testing that the transaction's sequence number do not have + // this bit set prevents using this property to get around a + // CHECKSEQUENCEVERIFY check. + if (txToSequence & this.SEQUENCE_LOCKTIME_DISABLE_FLAG) { + return false; + } - // Mask off any bits that do not have consensus-enforced meaning before - // doing the integer comparisons - var nLockTimeMask = - Interpreter.SEQUENCE_LOCKTIME_TYPE_FLAG | Interpreter.SEQUENCE_LOCKTIME_MASK; - var txToSequenceMasked = new BN(txToSequence & nLockTimeMask); - var nSequenceMasked = nSequence.and(nLockTimeMask); + // Mask off any bits that do not have consensus-enforced meaning before + // doing the integer comparisons + const nLockTimeMask = + Interpreter.SEQUENCE_LOCKTIME_TYPE_FLAG | Interpreter.SEQUENCE_LOCKTIME_MASK; + const txToSequenceMasked = new BN(txToSequence & nLockTimeMask); + const nSequenceMasked = nSequence.and(nLockTimeMask); - // There are two kinds of nSequence: lock-by-blockheight and - // lock-by-blocktime, distinguished by whether nSequenceMasked < - // CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG. - // - // We want to compare apples to apples, so fail the script unless the type - // of nSequenceMasked being tested is the same as the nSequenceMasked in the - // transaction. - var SEQUENCE_LOCKTIME_TYPE_FLAG_BN = new BN(Interpreter.SEQUENCE_LOCKTIME_TYPE_FLAG); + // There are two kinds of nSequence: lock-by-blockheight and + // lock-by-blocktime, distinguished by whether nSequenceMasked < + // CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG. + // + // We want to compare apples to apples, so fail the script unless the type + // of nSequenceMasked being tested is the same as the nSequenceMasked in the + // transaction. + const SEQUENCE_LOCKTIME_TYPE_FLAG_BN = new BN(Interpreter.SEQUENCE_LOCKTIME_TYPE_FLAG); - if (!((txToSequenceMasked.lt(SEQUENCE_LOCKTIME_TYPE_FLAG_BN) && + if (!((txToSequenceMasked.lt(SEQUENCE_LOCKTIME_TYPE_FLAG_BN) && nSequenceMasked.lt(SEQUENCE_LOCKTIME_TYPE_FLAG_BN)) || (txToSequenceMasked.gte(SEQUENCE_LOCKTIME_TYPE_FLAG_BN) && nSequenceMasked.gte(SEQUENCE_LOCKTIME_TYPE_FLAG_BN)))) { - return false; - } + return false; + } - // Now that we know we're comparing apples-to-apples, the comparison is a - // simple numeric one. - if (nSequenceMasked.gt(txToSequenceMasked)) { - return false; - } - return true; + // Now that we know we're comparing apples-to-apples, the comparison is a + // simple numeric one. + if (nSequenceMasked.gt(txToSequenceMasked)) { + return false; } + return true; +}; /** * Implemented from bitcoin-abc @@ -747,21 +746,21 @@ Interpreter.prototype.checkSequence = function(nSequence) { * @param {*} dummy * @param {*} size */ -function DecodeBitfield(dummy, size) { +Interpreter.prototype._decodeBitfield = function(dummy, size) { if (size > 32) { - this.errstr = "INVALID_BITFIELD_SIZE"; - return {result: false}; + this.errstr = 'INVALID_BITFIELD_SIZE'; + return { result: false }; } - let bitfieldSize = Math.floor((size + 7) / 8); - let dummyBitlength = dummy.length; + const bitfieldSize = Math.floor((size + 7) / 8); + const dummyBitlength = dummy.length; if (dummyBitlength !== bitfieldSize) { - this.errstr = "INVALID_BITFIELD_SIZE"; - return {result: false}; + this.errstr = 'INVALID_BITFIELD_SIZE'; + return { result: false }; } let bitfield = 0; - let dummyAs32Bit = Uint32Array.from(dummy); + const dummyAs32Bit = Uint32Array.from(dummy); // let one = new Uint8Array([1]); // let oneAs64Bit = BigUint64Array.from(one); @@ -769,14 +768,14 @@ function DecodeBitfield(dummy, size) { bitfield = bitfield | (dummyAs32Bit[i] << (8*i)); } - let mask = (0x01 << size) - 1 - if((bitfield & mask) != bitfield) { - this.errstr = "INVALID_BIT_RANGE"; - return {result: false}; + const mask = (0x01 << size) - 1; + if ((bitfield & mask) != bitfield) { + this.errstr = 'INVALID_BIT_RANGE'; + return { result: false }; } - return {result: true, bitfield: bitfield}; -} + return { result: true, bitfield: bitfield }; +}; /** * countBits @@ -792,17 +791,18 @@ function countBits(v) { * More detailed explanation can be found at * https://www.playingwithpointers.com/blog/swar.html */ - v = v - (((v) >> 1) & 0x55555555); - v = (v & 0x33333333) + ((v >> 2) & 0x33333333); - return ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; + v = v - (((v) >> 1) & 0x55555555); + v = (v & 0x33333333) + ((v >> 2) & 0x33333333); + return ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; } /** * Based on the inner loop of bitcoind's EvalScript function * bitcoind commit: b5d1b1092998bc95313856d535c632ea5a8f9104 */ -Interpreter.prototype.step = function() { - var self = this; +Interpreter.prototype.step = function () { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; function stacktop(i) { return self.stack[self.stack.length+i]; @@ -850,16 +850,16 @@ Interpreter.prototype.step = function() { const fNativeTokens = (this.flags & Interpreter.SCRIPT_ENABLE_TOKENS) !== 0; const maxScriptIntegerSize = f64BitIntegers ? 8 : 4; - //bool fExec = !count(vfExec.begin(), vfExec.end(), false); - var fExec = (this.vfExec.indexOf(false) === -1); - var buf, buf1, buf2, spliced, n, x1, x2, bn, bn1, bn2, bufSig, bufPubkey, bufMessage, subscript; - var sig, pubkey; - var fValue, fSuccess; + // bool fExec = !count(vfExec.begin(), vfExec.end(), false); + const fExec = (this.vfExec.indexOf(false) === -1); + let buf, buf1, buf2, spliced, n, x1, x2, bn, bn1, bn2, bufSig, bufPubkey, bufMessage, subscript; + let sig, pubkey; + let fValue, fSuccess; // Read instruction - var chunk = this.script.chunks[this.pc]; + const chunk = this.script.chunks[this.pc]; this.pc++; - var opcodenum = chunk.opcodenum; + const opcodenum = chunk.opcodenum; if (_.isUndefined(opcodenum)) { this.errstr = 'SCRIPT_ERR_UNDEFINED_OPCODE'; return false; @@ -1117,16 +1117,14 @@ Interpreter.prototype.step = function() { break; case Opcode.OP_RETURN: - { - this.errstr = 'SCRIPT_ERR_OP_RETURN'; - return false; - } - break; - + { + this.errstr = 'SCRIPT_ERR_OP_RETURN'; + return false; + } - // - // Stack ops - // + // + // Stack ops + // case Opcode.OP_TOALTSTACK: { if (this.stack.length < 1) { @@ -1182,7 +1180,7 @@ Interpreter.prototype.step = function() { } buf1 = stacktop(-3); buf2 = stacktop(-2); - var buf3 = stacktop(-1); + const buf3 = stacktop(-1); this.stack.push(buf1); this.stack.push(buf2); this.stack.push(buf3); @@ -1313,7 +1311,7 @@ Interpreter.prototype.step = function() { this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; return false; } - buf = stacktop(-n-1); + buf = stacktop(-n-1); if (opcodenum === Opcode.OP_ROLL) { this.stack.splice(this.stack.length - n - 1, 1); } @@ -1332,7 +1330,7 @@ Interpreter.prototype.step = function() { } x1 = stacktop(-3); x2 = stacktop(-2); - var x3 = stacktop(-1); + const x3 = stacktop(-1); this.stack[this.stack.length - 3] = x2; this.stack[this.stack.length - 2] = x3; this.stack[this.stack.length - 1] = x1; @@ -1401,17 +1399,17 @@ Interpreter.prototype.step = function() { // To avoid allocating, we modify vch1 in place. switch (opcodenum) { case Opcode.OP_AND: - for (var i = 0; i < buf1.length; i++) { + for (let i = 0; i < buf1.length; i++) { buf1[i] &= buf2[i]; } break; case Opcode.OP_OR: - for (var i = 0; i < buf1.length; i++) { + for (let i = 0; i < buf1.length; i++) { buf1[i] |= buf2[i]; } break; case Opcode.OP_XOR: - for (var i = 0; i < buf1.length; i++) { + for (let i = 0; i < buf1.length; i++) { buf1[i] ^= buf2[i]; } break; @@ -1420,13 +1418,13 @@ Interpreter.prototype.step = function() { } // And pop vch2. - this.stack.pop() + this.stack.pop(); } break; case Opcode.OP_EQUAL: case Opcode.OP_EQUALVERIFY: - //case Opcode.OP_NOTEQUAL: // use Opcode.OP_NUMNOTEQUAL + // case Opcode.OP_NOTEQUAL: // use Opcode.OP_NUMNOTEQUAL { // (x1 x2 - bool) if (this.stack.length < 2) { @@ -1435,7 +1433,7 @@ Interpreter.prototype.step = function() { } buf1 = stacktop(-2); buf2 = stacktop(-1); - var fEqual = buf1.toString('hex') === buf2.toString('hex'); + const fEqual = buf1.toString('hex') === buf2.toString('hex'); this.stack.pop(); this.stack.pop(); this.stack.push(fEqual ? Interpreter.true : Interpreter.false); @@ -1489,7 +1487,7 @@ Interpreter.prototype.step = function() { case Opcode.OP_0NOTEQUAL: bn = new BN((bn.cmp(BN.Zero) !== 0) + 0); break; - //default: assert(!'invalid opcode'); break; // TODO: does this ever occur? + // default: assert(!'invalid opcode'); break; // TODO: does this ever occur? } this.stack.pop(); this.stack.push(bn.toScriptNumBuffer()); @@ -1621,8 +1619,8 @@ Interpreter.prototype.step = function() { } bn1 = BN.fromScriptNumBuffer(stacktop(-3), fRequireMinimal, maxScriptIntegerSize); bn2 = BN.fromScriptNumBuffer(stacktop(-2), fRequireMinimal, maxScriptIntegerSize); - var bn3 = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal, maxScriptIntegerSize); - //bool fValue = (bn2 <= bn1 && bn1 < bn3); + const bn3 = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal, maxScriptIntegerSize); + // bool fValue = (bn2 <= bn1 && bn1 < bn3); fValue = (bn2.cmp(bn1) <= 0) && (bn1.cmp(bn3) < 0); this.stack.pop(); this.stack.pop(); @@ -1647,7 +1645,7 @@ Interpreter.prototype.step = function() { return false; } buf = stacktop(-1); - //valtype vchHash((opcode == Opcode.OP_RIPEMD160 || + // valtype vchHash((opcode == Opcode.OP_RIPEMD160 || // opcode == Opcode.OP_SHA1 || opcode == Opcode.OP_HASH160) ? 20 : 32); var bufHash; if (opcodenum === Opcode.OP_RIPEMD160) { @@ -1696,20 +1694,20 @@ Interpreter.prototype.step = function() { }); // Drop the signature, since there's no way for a signature to sign itself - var tmpScript = new Script().add(bufSig); + const tmpScript = new Script().add(bufSig); subscript.findAndDelete(tmpScript); try { sig = Signature.fromTxFormat(bufSig); pubkey = PublicKey.fromBuffer(bufPubkey, false); - if(!sig.isSchnorr) { + if (!sig.isSchnorr) { fSuccess = this.tx.verifySignature(sig, pubkey, this.nin, subscript, this.satoshisBN, this.flags); } else { fSuccess = this.tx.verifySignature(sig, pubkey, this.nin, subscript, this.satoshisBN, this.flags, 'schnorr'); } } catch (e) { - //invalid sig or pubkey + // invalid sig or pubkey fSuccess = false; } @@ -1771,7 +1769,7 @@ Interpreter.prototype.step = function() { fSuccess = Schnorr.verify(bufHash, sig, pubkey, 'big'); } } catch (e) { - //invalid sig or pubkey + // invalid sig or pubkey fSuccess = false; } @@ -1810,7 +1808,7 @@ Interpreter.prototype.step = function() { } buf1 = stacktop(-1); - var reversedBuf = Buffer.from(buf1).reverse(); + const reversedBuf = Buffer.from(buf1).reverse(); this.stack.pop(); this.stack.push(reversedBuf); } @@ -1821,15 +1819,15 @@ Interpreter.prototype.step = function() { { // ([dummy] [sig ...] num_of_signatures [pubkey ...] num_of_pubkeys -- bool) - var i = 1; - let idxTopKey = i + 1; + let i = 1; + const idxTopKey = i + 1; if (this.stack.length < i) { this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; return false; } - var nKeysCount = BN.fromScriptNumBuffer(stacktop(-i), fRequireMinimal).toNumber(); - var idxSigCount = idxTopKey + nKeysCount; + const nKeysCount = BN.fromScriptNumBuffer(stacktop(-i), fRequireMinimal).toNumber(); + const idxSigCount = idxTopKey + nKeysCount; if (nKeysCount < 0 || nKeysCount > 20) { this.errstr = 'SCRIPT_ERR_PUBKEY_COUNT'; return false; @@ -1842,8 +1840,8 @@ Interpreter.prototype.step = function() { // todo map interpreter.cpp variables with interpreter.js variables for future readability, maintainability // ikey maps to idxTopKey in interpreter.cpp (MULTISIG case) - var ikey = ++i; // top pubkey - var idxTopSig = idxSigCount + 1; + const ikey = ++i; // top pubkey + const idxTopSig = idxSigCount + 1; // i maps to idxSigCount in interpreter.cpp (MULTISIG case) (stack depth of nSigsCount) i += nKeysCount; @@ -1852,22 +1850,22 @@ Interpreter.prototype.step = function() { // the stack. Top stack item = 1. With // SCRIPT_VERIFY_NULLFAIL, this is used for cleanup if // operation fails. - var ikey2 = nKeysCount + 2; // ?dummy variable + let ikey2 = nKeysCount + 2; // ?dummy variable if (this.stack.length < i) { this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; return false; } - var nSigsCount = BN.fromScriptNumBuffer(stacktop(-idxSigCount), fRequireMinimal).toNumber(); - var idxDummy = idxTopSig + nSigsCount; + const nSigsCount = BN.fromScriptNumBuffer(stacktop(-idxSigCount), fRequireMinimal).toNumber(); + const idxDummy = idxTopSig + nSigsCount; if (nSigsCount < 0 || nSigsCount > nKeysCount) { this.errstr = 'SCRIPT_ERR_SIG_COUNT'; return false; } // int isig = ++i; - var isig = ++i; + const isig = ++i; i += nSigsCount; if (this.stack.length < idxDummy) { this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; @@ -1882,87 +1880,87 @@ Interpreter.prototype.step = function() { fSuccess = true; - if((this.flags & Interpreter.SCRIPT_ENABLE_SCHNORR_MULTISIG) && stacktop(-idxDummy).length !== 0) { + if ((this.flags & Interpreter.SCRIPT_ENABLE_SCHNORR_MULTISIG) && stacktop(-idxDummy).length !== 0) { // SCHNORR MULTISIG - let dummy = stacktop(-idxDummy); + const dummy = stacktop(-idxDummy); - let bitfieldObj = DecodeBitfield(dummy, nKeysCount); + const bitfieldObj = this._decodeBitfield(dummy, nKeysCount); - if(!bitfieldObj["result"]) { + if (!bitfieldObj['result']) { fSuccess = false; } - let nSigs8bit = new Uint8Array([nSigsCount]); - let nSigs32 = Uint32Array.from(nSigs8bit); + const nSigs8bit = new Uint8Array([nSigsCount]); + const nSigs32 = Uint32Array.from(nSigs8bit); - if (countBits(bitfieldObj["bitfield"]) !== nSigs32[0]) { - this.errstr = "INVALID_BIT_COUNT"; + if (countBits(bitfieldObj['bitfield']) !== nSigs32[0]) { + this.errstr = 'INVALID_BIT_COUNT'; fSuccess = false; } - var bottomKey = idxTopKey + nKeysCount - 1; - var bottomSig = idxTopSig + nSigsCount - 1; + const bottomKey = idxTopKey + nKeysCount - 1; + const bottomSig = idxTopSig + nSigsCount - 1; let iKey = 0; - for(let iSig = 0; iSig < nSigsCount; + for (let iSig = 0; iSig < nSigsCount; iSig++, iKey++) { - if((bitfieldObj["bitfield"] >> iKey) === 0) { - this.errstr = "INVALID_BIT_RANGE"; - fSuccess = false; - } - - while(((bitfieldObj["bitfield"] >> iKey) & 0x01) == 0) { - if(iKey >= nKeysCount) { - this.errstr = "wrong"; - fSuccess = false; - break; - } - iKey++; - } + if ((bitfieldObj['bitfield'] >> iKey) === 0) { + this.errstr = 'INVALID_BIT_RANGE'; + fSuccess = false; + } - // this is a sanity check and should be - // unreachable - if(iKey >= nKeysCount) { - this.errstr = "PUBKEY_COUNT"; + while (((bitfieldObj['bitfield'] >> iKey) & 0x01) == 0) { + if (iKey >= nKeysCount) { + this.errstr = 'wrong'; fSuccess = false; + break; } + iKey++; + } - // Check the signature - let bufsig = stacktop(-bottomSig + iSig) - let bufPubkey = stacktop(-bottomKey + iKey) + // this is a sanity check and should be + // unreachable + if (iKey >= nKeysCount) { + this.errstr = 'PUBKEY_COUNT'; + fSuccess = false; + } - // Note that only pubkeys associated with a - // signature are check for validity + // Check the signature + const bufsig = stacktop(-bottomSig + iSig); + const bufPubkey = stacktop(-bottomKey + iKey); - if(!this.checkRawSignatureEncoding(bufsig) || !this.checkPubkeyEncoding(bufPubkey)) { - fSuccess = false; - } + // Note that only pubkeys associated with a + // signature are check for validity - let sig = Signature.fromTxFormat(bufsig); - let pubkey = PublicKey.fromBuffer(bufPubkey, false); - let fOk = this.tx.verifySignature(sig, pubkey, this.nin, subscript, this.satoshisBN, this.flags, "schnorr"); + if (!this.checkRawSignatureEncoding(bufsig) || !this.checkPubkeyEncoding(bufPubkey)) { + fSuccess = false; + } - if(!fOk) { - this.errstr = "SIG_NULLFAIL" - fSuccess = false; - } + const sig = Signature.fromTxFormat(bufsig); + const pubkey = PublicKey.fromBuffer(bufPubkey, false); + const fOk = this.tx.verifySignature(sig, pubkey, this.nin, subscript, this.satoshisBN, this.flags, 'schnorr'); - if (bufsig.length) { - this.nSigChecks += 1; - } + if (!fOk) { + this.errstr = 'SIG_NULLFAIL'; + fSuccess = false; } - if ((bitfieldObj["bitfield"] >> iKey) != 0) { - // This is a sanity check and should be - // unreachable. - this.errstr = "INVALID_BIT_COUNT" - fSuccess = false; + if (bufsig.length) { + this.nSigChecks += 1; } + } + + if ((bitfieldObj['bitfield'] >> iKey) != 0) { + // This is a sanity check and should be + // unreachable. + this.errstr = 'INVALID_BIT_COUNT'; + fSuccess = false; + } } else { // Drop the signatures, since there's no way for a signature to sign itself - for (var k = 0; k < nSigsCount; k++) { + for (let k = 0; k < nSigsCount; k++) { bufSig = stacktop(-isig-k); subscript.findAndDelete(new Script().add(bufSig)); } @@ -1970,49 +1968,49 @@ Interpreter.prototype.step = function() { let nSigsRemaining = nSigsCount; let nKeysRemaining = nKeysCount; while (fSuccess && nSigsRemaining > 0) { - bufSig = stacktop(-isig - (nSigsCount - nSigsRemaining)); - if (bufSig.length === 65) { - return false; - } - bufPubkey = stacktop(-ikey - (nKeysCount - nKeysRemaining)); - - if (!this.checkTxSignatureEncoding(bufSig) || !this.checkPubkeyEncoding(bufPubkey)) { - return false; - } + bufSig = stacktop(-isig - (nSigsCount - nSigsRemaining)); + if (bufSig.length === 65) { + return false; + } + bufPubkey = stacktop(-ikey - (nKeysCount - nKeysRemaining)); - var fOk; - try { - sig = Signature.fromTxFormat(bufSig); - pubkey = PublicKey.fromBuffer(bufPubkey, false); - fOk = this.tx.verifySignature(sig, pubkey, this.nin, subscript, this.satoshisBN, this.flags); - } catch (e) { - //invalid sig or pubkey - fOk = false; - } + if (!this.checkTxSignatureEncoding(bufSig) || !this.checkPubkeyEncoding(bufPubkey)) { + return false; + } - if (fOk) { - nSigsRemaining--; - } - nKeysRemaining--; + var fOk; + try { + sig = Signature.fromTxFormat(bufSig); + pubkey = PublicKey.fromBuffer(bufPubkey, false); + fOk = this.tx.verifySignature(sig, pubkey, this.nin, subscript, this.satoshisBN, this.flags); + } catch (e) { + // invalid sig or pubkey + fOk = false; + } - // If there are more signatures left than keys left, - // then too many signatures have failed - if (nSigsRemaining > nKeysRemaining) { - fSuccess = false; - } + if (fOk) { + nSigsRemaining--; } + nKeysRemaining--; - let areAllSignaturesNull = true; - for (let l = 0; l < nSigsCount; l++) { - if (stacktop(-isig-l) && stacktop(-isig-l).length) { - areAllSignaturesNull = false; - break; - } + // If there are more signatures left than keys left, + // then too many signatures have failed + if (nSigsRemaining > nKeysRemaining) { + fSuccess = false; } + } - if (!areAllSignaturesNull) { - this.nSigChecks += nKeysCount; + let areAllSignaturesNull = true; + for (let l = 0; l < nSigsCount; l++) { + if (stacktop(-isig-l) && stacktop(-isig-l).length) { + areAllSignaturesNull = false; + break; } + } + + if (!areAllSignaturesNull) { + this.nSigChecks += nKeysCount; + } } // Clean up stack of actual arguments @@ -2063,360 +2061,361 @@ Interpreter.prototype.step = function() { // // Byte string operations // - case Opcode.OP_CAT: { + case Opcode.OP_CAT: { - if (this.stack.length < 2) { - this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; - return false; - } + if (this.stack.length < 2) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } - buf1 = stacktop(-2); - buf2 = stacktop(-1); - if (buf1.length + buf2.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { - this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; - return false; - } - this.stack[this.stack.length - 2] = Buffer.concat([buf1,buf2]); - this.stack.pop(); + buf1 = stacktop(-2); + buf2 = stacktop(-1); + if (buf1.length + buf2.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { + this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; + return false; } + this.stack[this.stack.length - 2] = Buffer.concat([buf1, buf2]); + this.stack.pop(); + } break; - case Opcode.OP_SPLIT: { - if (this.stack.length < 2) { - this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; - return false; - } - buf1 = stacktop(-2); + case Opcode.OP_SPLIT: { + if (this.stack.length < 2) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } + buf1 = stacktop(-2); - // Make sure the split point is apropriate. - var position = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal).toNumber(); - if (position < 0 || position > buf1.length) { - this.errstr = 'SCRIPT_ERR_INVALID_SPLIT_RANGE'; - return false; - } + // Make sure the split point is apropriate. + const position = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal).toNumber(); + if (position < 0 || position > buf1.length) { + this.errstr = 'SCRIPT_ERR_INVALID_SPLIT_RANGE'; + return false; + } - // Prepare the results in their own buffer as `data` - // will be invalidated. - // Copy buffer data, to slice it before - var n1 = Buffer.from(buf1); + // Prepare the results in their own buffer as `data` + // will be invalidated. + // Copy buffer data, to slice it before + const n1 = Buffer.from(buf1); - // Replace existing stack values by the new values. - this.stack[this.stack.length - 2] = n1.slice(0, position); - this.stack[this.stack.length - 1] = n1.slice(position); - } + // Replace existing stack values by the new values. + this.stack[this.stack.length - 2] = n1.slice(0, position); + this.stack[this.stack.length - 1] = n1.slice(position); + } break; // // Conversion operations // - case Opcode.OP_NUM2BIN: { + case Opcode.OP_NUM2BIN: { - // (in -- out) - if (this.stack.length < 2) { - this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; - return false; - } + // (in -- out) + if (this.stack.length < 2) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } - var size = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal).toNumber(); - if (size > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { - this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; - return false; - } + const size = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal).toNumber(); + if (size > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { + this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; + return false; + } - this.stack.pop(); - var rawnum = stacktop(-1); + this.stack.pop(); + let rawnum = stacktop(-1); - // Try to see if we can fit that number in the number of - // byte requested. - rawnum=Interpreter._minimallyEncode(rawnum); + // Try to see if we can fit that number in the number of + // byte requested. + rawnum=Interpreter._minimallyEncode(rawnum); - if (rawnum.length > size) { - // We definitively cannot. - this.errstr = 'SCRIPT_ERR_IMPOSSIBLE_ENCODING'; - return false; - } + if (rawnum.length > size) { + // We definitively cannot. + this.errstr = 'SCRIPT_ERR_IMPOSSIBLE_ENCODING'; + return false; + } - // We already have an element of the right size, we - // don't need to do anything. - if (rawnum.length == size) { - this.stack[this.stack.length-1] = rawnum; - break; - } + // We already have an element of the right size, we + // don't need to do anything. + if (rawnum.length == size) { + this.stack[this.stack.length-1] = rawnum; + break; + } - var signbit = 0x00; - if (rawnum.length > 0) { - signbit = rawnum[rawnum.length - 1] & 0x80; - rawnum[rawnum.length - 1] &= 0x7f; - } + let signbit = 0x00; + if (rawnum.length > 0) { + signbit = rawnum[rawnum.length - 1] & 0x80; + rawnum[rawnum.length - 1] &= 0x7f; + } - var num = Buffer.alloc(size); - rawnum.copy(num,0); + const num = Buffer.alloc(size); + rawnum.copy(num, 0); - var l = rawnum.length - 1; - while (l++ < size - 2) { - num[l]=0x00; - } + let l = rawnum.length - 1; + while (l++ < size - 2) { + num[l]=0x00; + } - num[l]=signbit; + num[l]=signbit; - this.stack[this.stack.length-1] = num; - } + this.stack[this.stack.length-1] = num; + } break; - case Opcode.OP_BIN2NUM: { - // (in -- out) - if (this.stack.length < 1) { - this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; - return false; - } + case Opcode.OP_BIN2NUM: { + // (in -- out) + if (this.stack.length < 1) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } - buf1 = stacktop(-1); - buf2 = Interpreter._minimallyEncode(buf1); + buf1 = stacktop(-1); + buf2 = Interpreter._minimallyEncode(buf1); - this.stack[this.stack.length - 1] = buf2; + this.stack[this.stack.length - 1] = buf2; - // The resulting number must be a valid number. - if (!Interpreter._isMinimallyEncoded(buf2, maxScriptIntegerSize)) { - this.errstr = 'SCRIPT_ERR_INVALID_NUMBER_RANGE'; - return false; - } + // The resulting number must be a valid number. + if (!Interpreter._isMinimallyEncoded(buf2, maxScriptIntegerSize)) { + this.errstr = 'SCRIPT_ERR_INVALID_NUMBER_RANGE'; + return false; } + } break; // Native Introspection opcodes (Nullary) - case Opcode.OP_INPUTINDEX: - case Opcode.OP_ACTIVEBYTECODE: - case Opcode.OP_TXVERSION: - case Opcode.OP_TXINPUTCOUNT: - case Opcode.OP_TXOUTPUTCOUNT: - case Opcode.OP_TXLOCKTIME: { - if (!fNativeIntrospection) { - this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; - return false; - } - if (!this.tx || !this.tx.inputs.every(input => input.output)) { - this.errstr = 'SCRIPT_ERR_CONTEXT_NOT_PRESENT'; - return false; - } - - switch (opcodenum) { - case Opcode.OP_INPUTINDEX: { - const bn = BN.fromNumber(this.nin); - this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_ACTIVEBYTECODE: { - // Subset of script starting at the most recent code separator (if any) - // or the entire script if no code separators are present. - subscript = new Script().set({ - chunks: this.script.chunks.slice(this.pbegincodehash) - }); - this.stack.push(subscript.toBuffer()); - } break; - case Opcode.OP_TXVERSION: { - const bn = BN.fromNumber(this.tx.version); - this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_TXINPUTCOUNT: { - const bn = BN.fromNumber(this.tx.inputs.length); - this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_TXOUTPUTCOUNT: { - const bn = BN.fromNumber(this.tx.outputs.length); - this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_TXLOCKTIME: { - const bn = BN.fromNumber(this.tx.nLockTime); - this.stack.push(bn.toScriptNumBuffer()); - } break; - default: { - this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; - return false; - } - } - } break; // end of Native Introspection opcodes (Nullary) + case Opcode.OP_INPUTINDEX: + case Opcode.OP_ACTIVEBYTECODE: + case Opcode.OP_TXVERSION: + case Opcode.OP_TXINPUTCOUNT: + case Opcode.OP_TXOUTPUTCOUNT: + case Opcode.OP_TXLOCKTIME: { + if (!fNativeIntrospection) { + this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; + return false; + } + if (!this.tx || !this.tx.inputs.every(input => input.output)) { + this.errstr = 'SCRIPT_ERR_CONTEXT_NOT_PRESENT'; + return false; + } - // Native Introspection opcodes (Unary) - case Opcode.OP_UTXOTOKENCATEGORY: - case Opcode.OP_UTXOTOKENCOMMITMENT: - case Opcode.OP_UTXOTOKENAMOUNT: - case Opcode.OP_OUTPUTTOKENCATEGORY: - case Opcode.OP_OUTPUTTOKENCOMMITMENT: - case Opcode.OP_OUTPUTTOKENAMOUNT: - if (!fNativeTokens) { + switch (opcodenum) { + case Opcode.OP_INPUTINDEX: { + const bn = BN.fromNumber(this.nin); + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_ACTIVEBYTECODE: { + // Subset of script starting at the most recent code separator (if any) + // or the entire script if no code separators are present. + subscript = new Script().set({ + chunks: this.script.chunks.slice(this.pbegincodehash) + }); + this.stack.push(subscript.toBuffer()); + } break; + case Opcode.OP_TXVERSION: { + const bn = BN.fromNumber(this.tx.version); + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_TXINPUTCOUNT: { + const bn = BN.fromNumber(this.tx.inputs.length); + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_TXOUTPUTCOUNT: { + const bn = BN.fromNumber(this.tx.outputs.length); + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_TXLOCKTIME: { + const bn = BN.fromNumber(this.tx.nLockTime); + this.stack.push(bn.toScriptNumBuffer()); + } break; + default: { this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; return false; } - case Opcode.OP_UTXOVALUE: - case Opcode.OP_UTXOBYTECODE: - case Opcode.OP_OUTPOINTTXHASH: - case Opcode.OP_OUTPOINTINDEX: - case Opcode.OP_INPUTBYTECODE: - case Opcode.OP_INPUTSEQUENCENUMBER: - case Opcode.OP_OUTPUTVALUE: - case Opcode.OP_OUTPUTBYTECODE: { - if (!fNativeIntrospection) { - this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; - return false; - } - if (!this.tx || !this.tx.inputs.every(input => input.output)) { - this.errstr = 'SCRIPT_ERR_CONTEXT_NOT_PRESENT'; - return false; - } - const bn = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal, maxScriptIntegerSize); - const index = bn.toNumber(); - this.stack.pop(); - - const indexType = [ - Opcode.OP_OUTPUTVALUE, - Opcode.OP_OUTPUTBYTECODE, - Opcode.OP_OUTPUTTOKENCATEGORY, - Opcode.OP_OUTPUTTOKENCOMMITMENT, - Opcode.OP_OUTPUTTOKENAMOUNT - ].includes(opcodenum) ? 'OUTPUT' : 'INPUT'; - - const maxIndex = indexType === 'OUTPUT' - ? this.tx.outputs.length - : this.tx.inputs.length; + } + } break; // end of Native Introspection opcodes (Nullary) - if (index < 0 || index >= maxIndex) { - this.errstr = `SCRIPT_ERR_INVALID_TX_${indexType}_INDEX`; - return false; - } + // Native Introspection opcodes (Unary) + case Opcode.OP_UTXOTOKENCATEGORY: + case Opcode.OP_UTXOTOKENCOMMITMENT: + case Opcode.OP_UTXOTOKENAMOUNT: + case Opcode.OP_OUTPUTTOKENCATEGORY: + case Opcode.OP_OUTPUTTOKENCOMMITMENT: + case Opcode.OP_OUTPUTTOKENAMOUNT: + if (!fNativeTokens) { + this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; + return false; + } + // eslint-disable-next-line no-fallthrough + case Opcode.OP_UTXOVALUE: + case Opcode.OP_UTXOBYTECODE: + case Opcode.OP_OUTPOINTTXHASH: + case Opcode.OP_OUTPOINTINDEX: + case Opcode.OP_INPUTBYTECODE: + case Opcode.OP_INPUTSEQUENCENUMBER: + case Opcode.OP_OUTPUTVALUE: + case Opcode.OP_OUTPUTBYTECODE: { + if (!fNativeIntrospection) { + this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; + return false; + } + if (!this.tx || !this.tx.inputs.every(input => input.output)) { + this.errstr = 'SCRIPT_ERR_CONTEXT_NOT_PRESENT'; + return false; + } + const bn = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal, maxScriptIntegerSize); + const index = bn.toNumber(); + this.stack.pop(); + + const indexType = [ + Opcode.OP_OUTPUTVALUE, + Opcode.OP_OUTPUTBYTECODE, + Opcode.OP_OUTPUTTOKENCATEGORY, + Opcode.OP_OUTPUTTOKENCOMMITMENT, + Opcode.OP_OUTPUTTOKENAMOUNT + ].includes(opcodenum) ? 'OUTPUT' : 'INPUT'; + + const maxIndex = indexType === 'OUTPUT' + ? this.tx.outputs.length + : this.tx.inputs.length; + + if (index < 0 || index >= maxIndex) { + this.errstr = `SCRIPT_ERR_INVALID_TX_${indexType}_INDEX`; + return false; + } - const tokenCapabilities = { - mutable: 1, - minting: 2, - }; + const tokenCapabilities = { + mutable: 1, + minting: 2, + }; - switch (opcodenum) { - case Opcode.OP_UTXOVALUE: { - const bn = this.tx.inputs[index].output.satoshisBN; - if (bn.getSize() > maxScriptIntegerSize) { - this.errstr = 'SCRIPT_ERR_INTEGER_SIZE'; - return false; - } + switch (opcodenum) { + case Opcode.OP_UTXOVALUE: { + const bn = this.tx.inputs[index].output.satoshisBN; + if (bn.getSize() > maxScriptIntegerSize) { + this.errstr = 'SCRIPT_ERR_INTEGER_SIZE'; + return false; + } + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_UTXOBYTECODE: { + const bytecode = this.tx.inputs[index].output.script.toBuffer(); + if (bytecode.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { + this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; + return false; + } + this.stack.push(bytecode); + } break; + case Opcode.OP_OUTPOINTTXHASH: { + const writer = new BufferWriter(); + writer.writeReverse(this.tx.inputs[index].prevTxId); + this.stack.push(writer.toBuffer()); + } break; + case Opcode.OP_OUTPOINTINDEX: { + const bn = BN.fromNumber(this.tx.inputs[index].outputIndex); + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_INPUTBYTECODE: { + const bytecode = this.tx.inputs[index].script.toBuffer(); + if (bytecode.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { + this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; + return false; + } + this.stack.push(bytecode); + } break; + case Opcode.OP_INPUTSEQUENCENUMBER: { + const bn = BN.fromNumber(this.tx.inputs[index].sequenceNumber); + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_OUTPUTVALUE: { + const bn = this.tx.outputs[index].satoshisBN; + if (bn.getSize() > maxScriptIntegerSize) { + this.errstr = 'SCRIPT_ERR_INTEGER_SIZE'; + return false; + } + this.stack.push(bn.toScriptNumBuffer()); + } break; + case Opcode.OP_OUTPUTBYTECODE: { + const bytecode = this.tx.outputs[index].script.toBuffer(); + if (bytecode.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { + this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; + return false; + } + this.stack.push(bytecode); + } break; + // Token introspection + case Opcode.OP_UTXOTOKENCATEGORY: { + const tokenData = this.tx.inputs[index].output.tokenData; + if (!tokenData) { + const bn = BN.fromNumber(0); this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_UTXOBYTECODE: { - const bytecode = this.tx.inputs[index].output.script.toBuffer(); - if (bytecode.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { - this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; - return false; - } - this.stack.push(bytecode); - } break; - case Opcode.OP_OUTPOINTTXHASH: { - const writer = new BufferWriter(); - writer.writeReverse(this.tx.inputs[index].prevTxId); - this.stack.push(writer.toBuffer()); - } break; - case Opcode.OP_OUTPOINTINDEX: { - const bn = BN.fromNumber(this.tx.inputs[index].outputIndex); + break; + } + const category = tokenData.category; + const capability = tokenData.nft && tokenData.nft.capability && tokenCapabilities[tokenData.nft.capability] || 0; + const capabilityBuf = BN.fromNumber(capability).toScriptNumBuffer(); + const categoryBuf = Buffer.from(category, 'hex').reverse(); + const fullBuffer = Buffer.concat([categoryBuf, capabilityBuf]); + this.stack.push(fullBuffer); + } break; + case Opcode.OP_UTXOTOKENCOMMITMENT: { + const tokenData = this.tx.inputs[index].output.tokenData; + if (!tokenData || !tokenData.nft) { + const bn = BN.fromNumber(0); this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_INPUTBYTECODE: { - const bytecode = this.tx.inputs[index].script.toBuffer(); - if (bytecode.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { - this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; - return false; - } - this.stack.push(bytecode); - } break; - case Opcode.OP_INPUTSEQUENCENUMBER: { - const bn = BN.fromNumber(this.tx.inputs[index].sequenceNumber); + break; + } + const commitment = tokenData.nft.commitment; + this.stack.push(Buffer.from(commitment, 'hex')); + } break; + case Opcode.OP_UTXOTOKENAMOUNT: { + const tokenData = this.tx.inputs[index].output.tokenData; + if (!tokenData || !tokenData.amount) { + const bn = BN.fromNumber(0); this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_OUTPUTVALUE: { - const bn = this.tx.outputs[index].satoshisBN; - if (bn.getSize() > maxScriptIntegerSize) { - this.errstr = 'SCRIPT_ERR_INTEGER_SIZE'; - return false; - } + break; + } + this.stack.push(tokenData.amount.toScriptNumBuffer()); + } break; + case Opcode.OP_OUTPUTTOKENCATEGORY: { + const tokenData = this.tx.outputs[index].tokenData; + if (!tokenData) { + const bn = BN.fromNumber(0); this.stack.push(bn.toScriptNumBuffer()); - } break; - case Opcode.OP_OUTPUTBYTECODE: { - const bytecode = this.tx.outputs[index].script.toBuffer(); - if (bytecode.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { - this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; - return false; - } - this.stack.push(bytecode); - } break; - // Token introspection - case Opcode.OP_UTXOTOKENCATEGORY: { - const tokenData = this.tx.inputs[index].output.tokenData; - if (!tokenData) { - const bn = BN.fromNumber(0); - this.stack.push(bn.toScriptNumBuffer()); - break; - } - const category = tokenData.category; - const capability = tokenData.nft && tokenData.nft.capability && tokenCapabilities[tokenData.nft.capability] || 0; - const capabilityBuf = BN.fromNumber(capability).toScriptNumBuffer(); - const categoryBuf = Buffer.from(category, 'hex').reverse(); - const fullBuffer = Buffer.concat([categoryBuf, capabilityBuf]); - this.stack.push(fullBuffer); - } break; - case Opcode.OP_UTXOTOKENCOMMITMENT: { - const tokenData = this.tx.inputs[index].output.tokenData; - if (!tokenData || !tokenData.nft) { - const bn = BN.fromNumber(0); - this.stack.push(bn.toScriptNumBuffer()); - break; - } - const commitment = tokenData.nft.commitment; - this.stack.push(Buffer.from(commitment, 'hex')); - } break; - case Opcode.OP_UTXOTOKENAMOUNT: { - const tokenData = this.tx.inputs[index].output.tokenData; - if (!tokenData || !tokenData.amount) { - const bn = BN.fromNumber(0); - this.stack.push(bn.toScriptNumBuffer()); - break; - } - this.stack.push(tokenData.amount.toScriptNumBuffer()); - } break; - case Opcode.OP_OUTPUTTOKENCATEGORY: { - const tokenData = this.tx.outputs[index].tokenData; - if (!tokenData) { - const bn = BN.fromNumber(0); - this.stack.push(bn.toScriptNumBuffer()); - break; - } - const category = tokenData.category; - const capability = tokenData.nft && tokenData.nft.capability && tokenCapabilities[tokenData.nft.capability] || 0; - const capabilityBuf = BN.fromNumber(capability).toScriptNumBuffer(); - const categoryBuf = Buffer.from(category, 'hex').reverse(); - const fullBuffer = Buffer.concat([categoryBuf, capabilityBuf]); - this.stack.push(fullBuffer); - } break; - case Opcode.OP_OUTPUTTOKENCOMMITMENT: { - const tokenData = this.tx.outputs[index].tokenData; - if (!tokenData || !tokenData.nft) { - const bn = BN.fromNumber(0); - this.stack.push(bn.toScriptNumBuffer()); - break; - } - const commitment = tokenData.nft.commitment; - this.stack.push(Buffer.from(commitment, 'hex')); - } break; - case Opcode.OP_OUTPUTTOKENAMOUNT: { - const tokenData = this.tx.outputs[index].tokenData; - if (!tokenData || !tokenData.amount) { - const bn = BN.fromNumber(0); - this.stack.push(bn.toScriptNumBuffer()); - break; - } - this.stack.push(tokenData.amount.toScriptNumBuffer()); - } break; - default: { - this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; - return false; + break; + } + const category = tokenData.category; + const capability = tokenData.nft && tokenData.nft.capability && tokenCapabilities[tokenData.nft.capability] || 0; + const capabilityBuf = BN.fromNumber(capability).toScriptNumBuffer(); + const categoryBuf = Buffer.from(category, 'hex').reverse(); + const fullBuffer = Buffer.concat([categoryBuf, capabilityBuf]); + this.stack.push(fullBuffer); + } break; + case Opcode.OP_OUTPUTTOKENCOMMITMENT: { + const tokenData = this.tx.outputs[index].tokenData; + if (!tokenData || !tokenData.nft) { + const bn = BN.fromNumber(0); + this.stack.push(bn.toScriptNumBuffer()); + break; + } + const commitment = tokenData.nft.commitment; + this.stack.push(Buffer.from(commitment, 'hex')); + } break; + case Opcode.OP_OUTPUTTOKENAMOUNT: { + const tokenData = this.tx.outputs[index].tokenData; + if (!tokenData || !tokenData.amount) { + const bn = BN.fromNumber(0); + this.stack.push(bn.toScriptNumBuffer()); + break; } + this.stack.push(tokenData.amount.toScriptNumBuffer()); + } break; + default: { + this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; + return false; } - } break; // end of Native Introspection opcodes (Unary) + } + } break; // end of Native Introspection opcodes (Unary) default: @@ -2427,4 +2426,3 @@ Interpreter.prototype.step = function() { return true; }; - From 60b61b1e1ee07f69937a972e7459495760d25b32 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Fri, 11 Sep 2026 16:19:55 -0400 Subject: [PATCH 13/14] CWC.Transactions keep scripts undefined in scripts durring standardization --- packages/crypto-wallet-core/src/transactions/btc/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index c7502315e69..ffebfa600c1 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -52,7 +52,7 @@ export class BTCTxProvider { satoshis: Number(utxo.satoshis ?? utxo.value ?? this.lib.Unit.fromBTC(utxo.amount ?? 0).toSatoshis()), txId: utxo.txId ?? utxo.mintTxid ?? utxo.txid, outputIndex: Number(utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout ?? 0), - script: utxo.scriptPubKey ?? new this.lib.Script(utxo.script).toHex(), + script: utxo.scriptPubKey ?? utxo.script ? new this.lib.Script(utxo.script).toHex() : undefined, address: utxo.address ? new this.lib.Address(utxo.address).toString() : undefined }; } From 0ca6cedd188a1ded37e50fb2d17b851d57fd71a3 Mon Sep 17 00:00:00 2001 From: Micah Maphet Date: Sat, 12 Sep 2026 11:52:54 -0400 Subject: [PATCH 14/14] fix bug in CWC utxo standardize function and use all libs to create utxos --- .../src/transactions/btc/index.ts | 2 +- .../test/transactions.test.ts | 40 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index ffebfa600c1..79e861732a4 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -52,7 +52,7 @@ export class BTCTxProvider { satoshis: Number(utxo.satoshis ?? utxo.value ?? this.lib.Unit.fromBTC(utxo.amount ?? 0).toSatoshis()), txId: utxo.txId ?? utxo.mintTxid ?? utxo.txid, outputIndex: Number(utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout ?? 0), - script: utxo.scriptPubKey ?? utxo.script ? new this.lib.Script(utxo.script).toHex() : undefined, + script: utxo.scriptPubKey ?? (utxo.script ? new this.lib.Script(utxo.script).toHex() : undefined), address: utxo.address ? new this.lib.Address(utxo.address).toString() : undefined }; } diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index 68fa463649c..f3e8c64e41d 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -78,20 +78,6 @@ describe('Transaction', function () { sequenceNumber: 4294967294 } ]; - const unspentOutputUtxos = [ - new bitcoreLib.Transaction.UnspentOutput({ - txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', - outputIndex: 1, - satoshis: 90_000, - script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' - }), - new bitcoreLib.Transaction.UnspentOutput({ - txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', - outputIndex: 0, - satoshis: 30_000, - script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' - }) - ]; const unspentOutputToObjectUtxos = [ { @@ -107,14 +93,28 @@ describe('Transaction', function () { scriptPubKey: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' } ]; - const utxoSet = [ - bitcoreNodeUtxos, - unspentOutputUtxos, - unspentOutputToObjectUtxos - ]; const recipients = [{ address: 'moVnNJpHHfssYJEnMTS5xXyGV8RhRQNRz5', amount: 100_000 }]; - for (const chain of ['BTC', 'BCH', 'DOGE', 'LTC']) { + for (const chain of ['BTC']) { + const unspentOutputUtxos = [ + new libs[chain].Transaction.UnspentOutput({ + txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + outputIndex: 1, + satoshis: 90_000, + script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' + }), + new libs[chain].Transaction.UnspentOutput({ + txId: '643ec66d6c4cad4cbdb8ed2166b8078975e0af9bb7ff7e30d394f43b0d9f18ab', + outputIndex: 0, + satoshis: 30_000, + script: '76a9144e744a19a009a9dd43a23a7c12045c83e82ac9d288ac' + }) + ]; + const utxoSet = [ + bitcoreNodeUtxos, + unspentOutputUtxos, + unspentOutputToObjectUtxos + ]; let tx: string; const lib = libs[chain]; it(`should create a tx with every utxo type for ${chain}`, () => {