diff --git a/src/shapes/wallet.ts b/src/shapes/wallet.ts index cd8f5f3..90f620e 100644 --- a/src/shapes/wallet.ts +++ b/src/shapes/wallet.ts @@ -3,7 +3,8 @@ import { TAddressTypeContent, IHeader, TAddressTypes, - IOnchainFees + IOnchainFees, + ECoinSelect } from '../types'; import cloneDeep from 'lodash.clonedeep'; import { @@ -61,7 +62,7 @@ export const defaultAddressContent: Readonly = { export const defaultSendTransaction: ISendTransaction = { outputs: [], inputs: [], - selectedInputs: [], + availableInputs: [], changeAddress: '', fiatAmount: 0, fee: 512, @@ -74,7 +75,8 @@ export const defaultSendTransaction: ISendTransaction = { max: false, tags: [], lightningInvoice: '', - selectedFeeId: EFeeId.none + selectedFeeId: EFeeId.none, + coinselect: ECoinSelect.default }; export const getDefaultSendTransaction = (): ISendTransaction => { diff --git a/src/transaction/index.ts b/src/transaction/index.ts index 2f2317c..ddc2f50 100644 --- a/src/transaction/index.ts +++ b/src/transaction/index.ts @@ -1,4 +1,4 @@ -import { coinselect, maxFunds } from '@bitcoinerlab/coinselect'; +import { coinselect as cs, maxFunds } from '@bitcoinerlab/coinselect'; import { DescriptorsFactory } from '@bitcoinerlab/descriptors'; import ecc, * as secp256k1 from '@bitcoinerlab/secp256k1'; import { BIP32Interface } from 'bip32'; @@ -11,6 +11,7 @@ import { getDefaultSendTransaction } from '../shapes'; import { EAddressType, EBoostType, + ECoinSelect, EFeeId, IAddInput, IAddresses, @@ -71,7 +72,8 @@ export class Transaction { utxos, rbf = false, satsPerByte = 1, - outputs + outputs, + coinselect = ECoinSelect.default }: ISetupTransaction = {}): Promise { try { const addressType = this._wallet.addressType; @@ -81,31 +83,20 @@ export class Transaction { const transaction = currentWallet.transaction; // Gather required inputs. - let selectedInputs: IUtxo[] = []; + let availableInputs: IUtxo[] = []; if (inputTxHashes) { // If specified, filter for the desired tx_hash and push the utxo as an input. - selectedInputs = currentWallet.utxos.filter((utxo) => { + availableInputs = currentWallet.utxos.filter((utxo) => { return inputTxHashes.includes(utxo.tx_hash); }); } else if (utxos) { - selectedInputs = utxos; - // } else { - // selectedInputs = currentWallet.utxos; + availableInputs = utxos; + } else { + availableInputs = currentWallet.utxos; } - selectedInputs = this.removeBlackListedUtxos(selectedInputs); - - const inputs = this.removeBlackListedUtxos(currentWallet.utxos); - - // if (!inputs.length) { - // // If inputs were previously selected, leave them. - // if (transaction.inputs.length > 0) { - // inputs = transaction.inputs; - // } else { - // // Otherwise, lets use our available utxo's. - // inputs = this.removeBlackListedUtxos(currentWallet.utxos); - // } - // } + availableInputs = this.removeBlackListedUtxos(availableInputs); + const inputs = this.removeBlackListedUtxos(availableInputs); if (!inputs.length) { return err('No inputs specified in setupTransaction.'); @@ -151,20 +142,21 @@ export class Transaction { message: '', transaction: { ...transaction, - selectedInputs, + availableInputs, inputs, outputs } }); const payload = { - selectedInputs, + availableInputs, inputs, changeAddress, fee, outputs, rbf, - satsPerByte + satsPerByte, + coinselect }; this._data = { @@ -181,13 +173,14 @@ export class Transaction { } } - updateCoinselect = ({ + recalculate = ({ + transaction = this.data, satsPerByte = this._data.satsPerByte }: { + transaction?: ISendTransaction; satsPerByte?: number; }): Result => { - const transaction = this._data; - const { max } = transaction; + const { availableInputs, coinselect } = transaction; try { const targets = transaction.outputs.map((output) => { @@ -200,93 +193,62 @@ export class Transaction { }; }); - if (max && transaction.outputs.length !== 1) { - throw new Error('Max send requires a single output.'); - } + let selection: ReturnType = undefined; - let selection: ReturnType = undefined; + const utxos = availableInputs.map((input) => { + return { + output: new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${input.address})` + }), + value: input.value + }; + }); - if (transaction.selectedInputs.length > 0) { - // use maxFunds algorithm if user selected inputs - const utxos = transaction.selectedInputs.map((input) => { - return { - output: new Output({ - network: networks[this._wallet.network], - descriptor: `addr(${input.address})` - }), - value: input.value - }; + if (coinselect === ECoinSelect.maxFunds) { + if (transaction.outputs.length !== 1) { + throw new Error('Max send requires a single output.'); + } + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.outputs[0].address})` + }); + selection = maxFunds({ + utxos, + targets: [], + remainder, + feeRate: satsPerByte + }); + } else if (coinselect === ECoinSelect.manual) { + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.changeAddress})` }); - if (max) { - const remainder = new Output({ - network: networks[this._wallet.network], - descriptor: `addr(${transaction.outputs[0].address})` - }); - selection = maxFunds({ - utxos, - targets: [], - remainder, - feeRate: satsPerByte - }); - } else { - const remainder = new Output({ - network: networks[this._wallet.network], - descriptor: `addr(${transaction.changeAddress})` - }); - selection = maxFunds({ - utxos, - targets, - remainder, - feeRate: satsPerByte - }); - } + selection = cs({ + utxos, + targets, + remainder, + feeRate: satsPerByte + }); } else { - // use all available utxos - const utxos = transaction.inputs.map((input) => { - return { - output: new Output({ - network: networks[this._wallet.network], - descriptor: `addr(${input.address})` - }), - value: input.value - }; + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.changeAddress})` + }); + selection = cs({ + utxos, + targets, + remainder, + feeRate: satsPerByte }); - - if (max) { - const remainder = new Output({ - network: networks[this._wallet.network], - descriptor: `addr(${transaction.outputs[0].address})` - }); - selection = maxFunds({ - utxos, - targets: [], - remainder, - feeRate: satsPerByte - }); - } else { - const remainder = new Output({ - network: networks[this._wallet.network], - descriptor: `addr(${transaction.changeAddress})` - }); - selection = coinselect({ - utxos, - targets, - remainder, - feeRate: satsPerByte - }); - } } if (selection === undefined) { throw new Error('Unable to find a suitable selection.'); } - const inputs = ( - transaction.selectedInputs.length > 0 - ? transaction.selectedInputs - : transaction.inputs - ).filter((oi) => { + const inputs = availableInputs.filter((oi) => { // Redundant check, just to make TS happy. if (selection === undefined) { throw new Error('Unable to find a suitable selection.'); @@ -357,6 +319,29 @@ export class Transaction { }); } + getTotalFeeNew = ({ + // message = '', + // fundingLightning = false + satsPerByte, + transaction = this.data + }: { + // message?: string; + // fundingLightning?: boolean; + satsPerByte: number; + transaction?: ISendTransaction; + }): number => { + const baseTransactionSize = TRANSACTION_DEFAULTS.recommendedBaseFee; + try { + const data = this.recalculate({ transaction, satsPerByte }); + if (data.isErr()) { + throw new Error(data.error.message); + } + return data.value.fee; + } catch { + return baseTransactionSize * satsPerByte; + } + }; + /** * Attempt to estimate the current fee for a given transaction and its UTXO's * @param {number} [satsPerByte] @@ -1058,7 +1043,7 @@ export class Transaction { satsPerByte: number; selectedFeeId?: EFeeId; }): Result<{ fee: number }> { - const updateRes = this.updateCoinselect({ satsPerByte }); + const updateRes = this.recalculate({ satsPerByte }); if (updateRes.isErr()) return err(updateRes.error.message); const transaction = updateRes.value; transaction.selectedFeeId = selectedFeeId; diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 7abfe29..05cc044 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -28,6 +28,7 @@ export interface ISetupTransaction { rbf?: boolean; // Enable or disable rbf satsPerByte?: number; // Used to specify the fee rate in sats per vbyte outputs?: IOutput[]; // Used to pre-specify outputs to use + coinselect?: ECoinSelect; // Used to specify the coin selection algorithm to use } export enum EFeeId { @@ -65,3 +66,10 @@ export type TGapLimitOptions = { lookAheadChange: number; lookBehindChange: number; }; + +// https://github.com/bitcoinerlab/coinselect#algorithms +export enum ECoinSelect { + default = 'default', + maxFunds = 'maxFunds', + manual = 'manual' // use all transaction.availableUtxos +} diff --git a/src/types/wallet.ts b/src/types/wallet.ts index 23050f0..1f541e5 100644 --- a/src/types/wallet.ts +++ b/src/types/wallet.ts @@ -1,16 +1,16 @@ +import { BIP32Interface } from 'bip32'; +import { ECPairInterface } from 'ecpair'; import { Result } from '../utils'; import { EElectrumNetworks, IHeader, INewBlock, Net, + Tls, TServer, - TTxResult, - Tls + TTxResult } from './electrum'; -import { EFeeId, TGapLimitOptions } from './transaction'; -import { ECPairInterface } from 'ecpair'; -import { BIP32Interface } from 'bip32'; +import { ECoinSelect, EFeeId, TGapLimitOptions } from './transaction'; export type TAvailableNetworks = 'bitcoin' | 'testnet' | 'regtest'; export type TAddressType = 'p2wpkh' | 'p2sh' | 'p2pkh'; @@ -122,8 +122,8 @@ export enum EBoostType { export interface ISendTransaction { outputs: IOutput[]; - selectedInputs: IUtxo[]; // use this if you want to specify which inputs to use. - inputs: IUtxo[]; + availableInputs: IUtxo[]; // inputs available to choose from. + inputs: IUtxo[]; // inputs to be used in the transaction. changeAddress: string; fiatAmount: number; fee: number; //Total fee in sats @@ -138,6 +138,7 @@ export interface ISendTransaction { tags: string[]; slashTagsUrl?: string; // TODO: Remove after migration. lightningInvoice?: string; // TODO: Remove after migration. + coinselect: ECoinSelect; } export interface IAddresses { diff --git a/tests/send.test.ts b/tests/send.test.ts index 867c837..e90780d 100644 --- a/tests/send.test.ts +++ b/tests/send.test.ts @@ -192,7 +192,7 @@ describe('Send', async function () { // TODO: check tx inputs and outputs }); - it.only('two inputs - two outputs, only one input should be used', async () => { + it('two inputs - two outputs, only one input should be used', async () => { const [a1, a2] = Object.values(wallet.data.addresses.p2wpkh).map( (v) => v.address );