diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 213bf0a123..1b08585bc5 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -93,6 +93,7 @@ import { getPolicyForEnv } from './descriptor/validatePolicy'; import { signTransaction } from './transaction/signTransaction'; import { isUtxoWalletData, UtxoWallet } from './wallet'; import { isDescriptorWalletData } from './descriptor/descriptorWallet'; +import type { Unspent } from './unspent'; import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; @@ -142,8 +143,6 @@ type UtxoCustomSigningFunction = { const { isChainCode, scriptTypeForChain, outputScripts } = bitgo; -type Unspent = bitgo.Unspent; - /** * Convert ValidationError to TxIntentMismatchRecipientError with structured data * diff --git a/modules/abstract-utxo/src/impl/doge/doge.ts b/modules/abstract-utxo/src/impl/doge/doge.ts index 547935711f..79fe2da15c 100644 --- a/modules/abstract-utxo/src/impl/doge/doge.ts +++ b/modules/abstract-utxo/src/impl/doge/doge.ts @@ -15,17 +15,18 @@ import { UtxoCoinName } from '../../names'; import { ParsedTransaction } from '../../transaction/types'; import type { TransactionExplanation } from '../../transaction/fixedScript/explainTransaction'; import type { CrossChainRecoverySigned, CrossChainRecoveryUnsigned } from '../../recovery/crossChainRecovery'; +import type { Unspent } from '../../unspent'; -type UnspentJSON = bitgo.Unspent & { valueString: string }; +type UnspentJSON = Unspent & { valueString: string }; type TransactionInfoJSON = TransactionInfo & { unspents: UnspentJSON[] }; type TransactionPrebuildJSON = TransactionPrebuild & { txInfo: TransactionInfoJSON }; function parseUnspents( - unspents: UnspentJSON[] | bitgo.Unspent[] -): bitgo.Unspent[] { - return unspents.map((unspent: bitgo.Unspent | UnspentJSON): bitgo.Unspent => { + unspents: UnspentJSON[] | Unspent[] +): Unspent[] { + return unspents.map((unspent: Unspent | UnspentJSON): Unspent => { if (typeof unspent.value === 'bigint') { - return unspent as bitgo.Unspent; + return unspent as Unspent; } if ('valueString' in unspent) { return { ...unspent, value: BigInt(unspent.valueString) }; diff --git a/modules/abstract-utxo/src/index.ts b/modules/abstract-utxo/src/index.ts index 641c0e7ec6..5c733d099a 100644 --- a/modules/abstract-utxo/src/index.ts +++ b/modules/abstract-utxo/src/index.ts @@ -4,6 +4,7 @@ export * from './config'; export * from './recovery'; export * from './transaction/fixedScript/replayProtection'; export * from './transaction/fixedScript/signLegacyTransaction'; +export * from './unspent'; export { UtxoWallet } from './wallet'; export * as descriptor from './descriptor'; diff --git a/modules/abstract-utxo/src/recovery/RecoveryProvider.ts b/modules/abstract-utxo/src/recovery/RecoveryProvider.ts index b6ee8da6f9..eb5bd3b127 100644 --- a/modules/abstract-utxo/src/recovery/RecoveryProvider.ts +++ b/modules/abstract-utxo/src/recovery/RecoveryProvider.ts @@ -1,9 +1,8 @@ import { BlockchairApi, AddressInfo, TransactionIO } from '@bitgo/blockapis'; -import { bitgo } from '@bitgo/utxo-lib'; -import { ApiNotImplementedError } from './baseApi'; +import type { Unspent } from '../unspent'; -type Unspent = bitgo.Unspent; +import { ApiNotImplementedError } from './baseApi'; /** * An account with bear minimum information required for recoveries. diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index 8cc494f322..fc4a783b9d 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -19,6 +19,7 @@ import { generateAddressWithChainAndIndex } from '../address'; import { encodeTransaction } from '../transaction/decode'; import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection'; import { isTestnetCoin, UtxoCoinName } from '../names'; +import type { WalletUnspent } from '../unspent'; import { forCoin, RecoveryProvider } from './RecoveryProvider'; import { MempoolApi } from './mempoolApi'; @@ -28,8 +29,7 @@ import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend, toPsbtToUt type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; type ChainCode = utxolib.bitgo.ChainCode; type RootWalletKeys = utxolib.bitgo.RootWalletKeys; -type WalletUnspent = utxolib.bitgo.WalletUnspent; -type WalletUnspentJSON = utxolib.bitgo.WalletUnspent & { +type WalletUnspentJSON = WalletUnspent & { valueString: string; }; diff --git a/modules/abstract-utxo/src/recovery/crossChainRecovery.ts b/modules/abstract-utxo/src/recovery/crossChainRecovery.ts index c33e8af81d..09550b9207 100644 --- a/modules/abstract-utxo/src/recovery/crossChainRecovery.ts +++ b/modules/abstract-utxo/src/recovery/crossChainRecovery.ts @@ -11,6 +11,7 @@ import { getNetworkFromCoinName, isTestnetCoin, UtxoCoinName } from '../names'; import { encodeTransaction } from '../transaction/decode'; import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection'; import { toTNumber } from '../tnumber'; +import type { Unspent, WalletUnspent } from '../unspent'; import { PsbtBackend, @@ -22,8 +23,6 @@ import { const { unspentSum } = utxolib.bitgo; type RootWalletKeys = utxolib.bitgo.RootWalletKeys; -type Unspent = utxolib.bitgo.Unspent; -type WalletUnspent = utxolib.bitgo.WalletUnspent; export interface BuildRecoveryTransactionOptions { wallet: string; diff --git a/modules/abstract-utxo/src/recovery/psbt.ts b/modules/abstract-utxo/src/recovery/psbt.ts index 9464735111..7069e792b2 100644 --- a/modules/abstract-utxo/src/recovery/psbt.ts +++ b/modules/abstract-utxo/src/recovery/psbt.ts @@ -3,9 +3,9 @@ import { Dimensions } from '@bitgo/unspents'; import { CoinName, fixedScriptWallet, utxolibCompat, address as wasmAddress } from '@bitgo/wasm-utxo'; import { getNetworkFromCoinName, UtxoCoinName } from '../names'; +import type { WalletUnspent } from '../unspent'; type RootWalletKeys = utxolib.bitgo.RootWalletKeys; -type WalletUnspent = utxolib.bitgo.WalletUnspent; const { chainCodesP2tr, chainCodesP2trMusig2 } = utxolib.bitgo; diff --git a/modules/abstract-utxo/src/transaction/explainTransaction.ts b/modules/abstract-utxo/src/transaction/explainTransaction.ts index df88abc868..c7d599654c 100644 --- a/modules/abstract-utxo/src/transaction/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/explainTransaction.ts @@ -6,6 +6,7 @@ import { getDescriptorMapFromWallet, isDescriptorWallet } from '../descriptor'; import { toBip32Triple } from '../keychains'; import { getPolicyForEnv } from '../descriptor/validatePolicy'; import { UtxoCoinName } from '../names'; +import type { Unspent } from '../unspent'; import { getReplayProtectionPubkeys } from './fixedScript/replayProtection'; import type { @@ -25,7 +26,7 @@ export function explainTx( params: { wallet?: IWallet; pubs?: string[]; - txInfo?: { unspents?: utxolib.bitgo.Unspent[] }; + txInfo?: { unspents?: Unspent[] }; changeInfo?: fixedScript.ChangeAddressInfo[]; }, coinName: UtxoCoinName diff --git a/modules/abstract-utxo/src/transaction/fixedScript/SigningError.ts b/modules/abstract-utxo/src/transaction/fixedScript/SigningError.ts index b137a1f446..97762e99dd 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/SigningError.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/SigningError.ts @@ -1,9 +1,7 @@ -import * as utxolib from '@bitgo/utxo-lib'; +import type { Unspent } from '../../unspent'; import type { PsbtParsedScriptType } from './signPsbtUtxolib'; -type Unspent = utxolib.bitgo.Unspent; - export class InputSigningError extends Error { static expectedWalletUnspent( inputIndex: number, diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts index 81d4292eca..052184a5a8 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts @@ -7,6 +7,7 @@ import * as utxocore from '@bitgo/utxo-core'; import type { Bip322Message } from '../../abstractUtxoCoin'; import type { Output, FixedScriptWalletOutput } from '../types'; +import type { Unspent } from '../../unspent'; import { toExtendedAddressFormat } from '../recipient'; import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey'; import { toBip32Triple } from '../../keychains'; @@ -192,7 +193,7 @@ function getPsbtInputSignaturesCount( function getTxInputSignaturesCount( tx: bitgo.UtxoTransaction, params: { - txInfo?: { unspents?: bitgo.Unspent[] }; + txInfo?: { unspents?: Unspent[] }; pubs?: bitgo.RootWalletKeys | string[]; }, coinName: UtxoCoinName @@ -444,7 +445,7 @@ export function explainLegacyTx( tx: bitgo.UtxoTransaction, params: { pubs?: string[]; - txInfo?: { unspents?: bitgo.Unspent[] }; + txInfo?: { unspents?: Unspent[] }; changeInfo?: { address: string; chain: number; index: number }[]; }, coinName: UtxoCoinName diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts index b21b40c0b8..9208ab97bc 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts @@ -7,6 +7,7 @@ import { isTriple, Triple } from '@bitgo/sdk-core'; import debugLib from 'debug'; import { UtxoCoinName } from '../../names'; +import type { Unspent, WalletUnspent } from '../../unspent'; import { getReplayProtectionAddresses } from './replayProtection'; import { InputSigningError, TransactionSigningError } from './SigningError'; @@ -15,8 +16,6 @@ const debug = debugLib('bitgo:v2:utxo'); const { isWalletUnspent, signInputWithUnspent, toOutput } = utxolib.bitgo; -type Unspent = utxolib.bitgo.Unspent; - type RootWalletKeys = utxolib.bitgo.RootWalletKeys; /** @@ -72,7 +71,7 @@ export function signAndVerifyWalletTransaction( return InputSigningError.expectedWalletUnspent(inputIndex, null, unspent); } try { - signInputWithUnspent(txBuilder, inputIndex, unspent, walletSigner); + signInputWithUnspent(txBuilder, inputIndex, unspent as WalletUnspent, walletSigner); debug('Successfully signed input %d of %d', inputIndex + 1, unspents.length); } catch (e) { return new InputSigningError(inputIndex, null, unspent, e); @@ -96,8 +95,10 @@ export function signAndVerifyWalletTransaction( if (!isWalletUnspent(unspent)) { return InputSigningError.expectedWalletUnspent(inputIndex, null, unspent); } + const walletUnspent = unspent as WalletUnspent; try { - const publicKey = walletSigner.deriveForChainAndIndex(unspent.chain, unspent.index).signer.publicKey; + const publicKey = walletSigner.deriveForChainAndIndex(walletUnspent.chain, walletUnspent.index).signer + .publicKey; if ( !utxolib.bitgo.verifySignatureWithPublicKey(signedTransaction, inputIndex, prevOutputs, publicKey) ) { @@ -124,7 +125,7 @@ export function signLegacyTransaction( params: { isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - txInfo: { unspents?: utxolib.bitgo.Unspent[] } | undefined; + txInfo: { unspents?: Unspent[] } | undefined; pubs: string[] | undefined; cosignerPub: string | undefined; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index d4925e43dd..0f66d23bd3 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -8,6 +8,7 @@ import * as utxolib from '@bitgo/utxo-lib'; import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { UtxoCoinName } from '../../names'; +import type { Unspent } from '../../unspent'; import { Musig2Participant } from './musig2'; import { signLegacyTransaction } from './signLegacyTransaction'; @@ -60,7 +61,7 @@ export async function signTransaction< coinName: UtxoCoinName, params: { walletId: string | undefined; - txInfo: { unspents?: utxolib.bitgo.Unspent[] } | undefined; + txInfo: { unspents?: Unspent[] } | undefined; isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; /** deprecated */ diff --git a/modules/abstract-utxo/src/unspent.ts b/modules/abstract-utxo/src/unspent.ts new file mode 100644 index 0000000000..2fdc43ba89 --- /dev/null +++ b/modules/abstract-utxo/src/unspent.ts @@ -0,0 +1,50 @@ +import * as utxolib from '@bitgo/utxo-lib'; + +/** + * Unspent transaction output (UTXO) type definition + * + * This type represents an unspent transaction output, independent from utxo-lib. + * The structure matches utxolib.bitgo.Unspent but is defined locally. + * + * @property id - Format: ${txid}:${vout}. Use `parseOutputId(id)` to parse. + * @property address - The network-specific encoded address. Use `toOutputScript(address, network)` to obtain scriptPubKey. + * @property value - The amount in satoshi. + */ +export interface Unspent { + /** + * Format: ${txid}:${vout}. + * Use `parseOutputId(id)` to parse. + */ + id: string; + /** + * The network-specific encoded address. + * Use `toOutputScript(address, network)` to obtain scriptPubKey. + */ + address: string; + /** + * The amount in satoshi. + */ + value: TNumber; +} + +/** + * Wallet unspent type - extends Unspent with wallet-specific fields + * + * This type includes all fields from Unspent plus: + * - chain: ChainCode (chain code for wallet derivation) + * - index: number (index for wallet derivation) + */ +export interface WalletUnspent extends Unspent { + chain: utxolib.bitgo.ChainCode; + index: number; +} + +/** + * Unspent with previous transaction data + * + * Extends Unspent with: + * - prevTx: Buffer (previous transaction data for legacy transactions) + */ +export interface UnspentWithPrevTx extends Unspent { + prevTx: Buffer; +} diff --git a/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts index 6043a5faf8..6f114472ce 100644 --- a/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts @@ -18,6 +18,7 @@ import { FormattedOfflineVaultTxInfo, } from '../../../src'; import { getCoinName } from '../../../src/names'; +import type { Unspent, WalletUnspent } from '../../../src/unspent'; import { defaultBitGo, encryptKeychain, @@ -35,7 +36,6 @@ import { import { MockRecoveryProvider } from './mock'; const { toOutput } = utxolib.bitgo; -type WalletUnspent = utxolib.bitgo.WalletUnspent; type RootWalletKeys = utxolib.bitgo.RootWalletKeys; type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; @@ -148,8 +148,8 @@ function run( sinon.restore(); }); - let recoverUnspents: utxolib.bitgo.Unspent[]; - let mockedApiUnspents: utxolib.bitgo.Unspent[]; + let recoverUnspents: Unspent[]; + let mockedApiUnspents: Unspent[]; before('create recovery data', async function () { this.timeout(10_000); @@ -267,7 +267,7 @@ function run( .map((u) => toOutput(u, coin.network)) .map((v) => ({ ...v, value: utxolib.bitgo.toTNumber(v.value, coin.amountType) })); tx.ins.forEach((input, inputIndex) => { - const unspent = recoverUnspents[inputIndex] as WalletUnspent; + const unspent = recoverUnspents[inputIndex] as WalletUnspent; const { publicKey } = rootKey.derivePath(walletKeys.getDerivationPath(rootKey, unspent.chain, unspent.index)); const signatures = utxolib.bitgo .getSignatureVerifications( diff --git a/modules/abstract-utxo/test/unit/recovery/crossChainRecovery.ts b/modules/abstract-utxo/test/unit/recovery/crossChainRecovery.ts index c778307020..e1cfc972de 100644 --- a/modules/abstract-utxo/test/unit/recovery/crossChainRecovery.ts +++ b/modules/abstract-utxo/test/unit/recovery/crossChainRecovery.ts @@ -17,6 +17,7 @@ import { convertLtcAddressToLegacyFormat, } from '../../../src'; import { isMainnetCoin, isTestnetCoin } from '../../../src/names'; +import type { Unspent, WalletUnspent } from '../../../src/unspent'; import { getFixture, keychainsBase58, @@ -34,8 +35,6 @@ import { getDefaultWalletUnspentSigner } from '../util/keychains'; import { MockCrossChainRecoveryProvider } from './mock'; -type WalletUnspent = utxolib.bitgo.WalletUnspent; - function getKeyId(k: KeychainBase58): string { return getSeed(k.pub).toString('hex'); } @@ -116,7 +115,7 @@ function run(sourceCoin: AbstractUtxoC let depositTx: utxolib.bitgo.UtxoTransaction; - function getDepositUnspents(): utxolib.bitgo.Unspent[] { + function getDepositUnspents(): Unspent[] { return [ mockUnspent( sourceCoin.network, diff --git a/modules/abstract-utxo/test/unit/recovery/mock.ts b/modules/abstract-utxo/test/unit/recovery/mock.ts index 96344177c0..5ea304a04f 100644 --- a/modules/abstract-utxo/test/unit/recovery/mock.ts +++ b/modules/abstract-utxo/test/unit/recovery/mock.ts @@ -6,8 +6,7 @@ import { address as wasmAddress, AddressFormat } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin, RecoveryProvider } from '../../../src'; import { Bch } from '../../../src/impl/bch'; import { Bsv } from '../../../src/impl/bsv'; - -type Unspent = bitgo.Unspent; +import type { Unspent, UnspentWithPrevTx } from '../../../src/unspent'; export class MockRecoveryProvider implements RecoveryProvider { public unspents: Unspent[]; private prevTxCache: Record = {}; @@ -16,7 +15,7 @@ export class MockRecoveryProvider implements RecoveryProvider { this.unspents.forEach((u) => { if (utxolib.bitgo.isUnspentWithPrevTx(u)) { const { txid } = bitgo.parseOutputId(u.id); - this.prevTxCache[txid] = u.prevTx.toString('hex'); + this.prevTxCache[txid] = (u as UnspentWithPrevTx).prevTx.toString('hex'); } }); } diff --git a/modules/abstract-utxo/test/unit/transaction.ts b/modules/abstract-utxo/test/unit/transaction.ts index 30b0ac3e74..2647303120 100644 --- a/modules/abstract-utxo/test/unit/transaction.ts +++ b/modules/abstract-utxo/test/unit/transaction.ts @@ -16,6 +16,7 @@ import { import { AbstractUtxoCoin, getReplayProtectionAddresses, generateAddress, getReplayProtectionPubkeys } from '../../src'; import { SdkBackend } from '../../src/transaction/types'; +import type { Unspent, WalletUnspent } from '../../src/unspent'; import { utxoCoins, @@ -35,9 +36,6 @@ import { defaultBitGo, } from './util'; -type Unspent = bitgo.Unspent; -type WalletUnspent = bitgo.WalletUnspent; - function getScriptTypes2Of3() { return [...bitgo.outputScripts.scriptTypes2Of3, 'taprootKeyPathSpend'] as const; } @@ -486,7 +484,8 @@ function run( assert.ok(utxolib.bitgo.getPsbtInputScriptType(input), 'p2shP2pk'); return; } - const pubkeys = walletKeys.deriveForChainAndIndex(unspent.chain, unspent.index).publicKeys; + const walletUnspent = unspent as WalletUnspent; + const pubkeys = walletKeys.deriveForChainAndIndex(walletUnspent.chain, walletUnspent.index).publicKeys; pubkeys.forEach((pk, pkIndex) => { psbt.validateSignaturesOfInputCommon(index, pk).should.eql(signedBy.includes(walletKeys.triple[pkIndex])); }); @@ -546,7 +545,7 @@ function run( async function testExplainTx( stageName: string, txHex: string, - unspents: utxolib.bitgo.Unspent[], + unspents: Unspent[], pubs: Triple | undefined ): Promise { const explanation = await coin.explainTransaction({ diff --git a/modules/abstract-utxo/test/unit/util/transaction.ts b/modules/abstract-utxo/test/unit/util/transaction.ts index e976cf37cf..b56ab1e4d6 100644 --- a/modules/abstract-utxo/test/unit/util/transaction.ts +++ b/modules/abstract-utxo/test/unit/util/transaction.ts @@ -2,9 +2,9 @@ import * as utxolib from '@bitgo/utxo-lib'; import { address as wasmAddress } from '@bitgo/wasm-utxo'; import { getCoinName } from '../../../src/names'; +import type { Unspent, WalletUnspent } from '../../../src/unspent'; const { isWalletUnspent, signInputWithUnspent } = utxolib.bitgo; type RootWalletKeys = utxolib.bitgo.RootWalletKeys; -type Unspent = utxolib.bitgo.Unspent; export type TransactionObj = { id: string; hex: string; @@ -94,7 +94,7 @@ function createTransactionBuilderWithSignedInputs { if (isWalletUnspent(u)) { - signInputWithUnspent(txBuilder, inputIndex, u, signer); + signInputWithUnspent(txBuilder, inputIndex, u as WalletUnspent, signer); } }); return txBuilder; diff --git a/modules/abstract-utxo/test/unit/util/unspents.ts b/modules/abstract-utxo/test/unit/util/unspents.ts index e974a9ba1c..c8ee30dee4 100644 --- a/modules/abstract-utxo/test/unit/util/unspents.ts +++ b/modules/abstract-utxo/test/unit/util/unspents.ts @@ -4,12 +4,11 @@ import * as wasmUtxo from '@bitgo/wasm-utxo'; import { getReplayProtectionAddresses } from '../../../src'; import { getCoinName } from '../../../src/names'; +import type { Unspent, WalletUnspent } from '../../../src/unspent'; const { scriptTypeForChain, chainCodesP2sh, getExternalChainCode, getInternalChainCode } = utxolib.bitgo; type RootWalletKeys = utxolib.bitgo.RootWalletKeys; -type Unspent = utxolib.bitgo.Unspent; -type WalletUnspent = utxolib.bitgo.WalletUnspent; type ChainCode = utxolib.bitgo.ChainCode; export type InputScriptType = utxolib.bitgo.outputScripts.ScriptType2Of3 | 'replayProtection';