Skip to content

Commit

Permalink
fix: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
limpbrains committed Dec 13, 2024
1 parent 6298af1 commit 108fa46
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 113 deletions.
8 changes: 5 additions & 3 deletions src/shapes/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
TAddressTypeContent,
IHeader,
TAddressTypes,
IOnchainFees
IOnchainFees,
ECoinSelect
} from '../types';
import cloneDeep from 'lodash.clonedeep';
import {
Expand Down Expand Up @@ -61,7 +62,7 @@ export const defaultAddressContent: Readonly<IAddress> = {
export const defaultSendTransaction: ISendTransaction = {
outputs: [],
inputs: [],
selectedInputs: [],
availableInputs: [],
changeAddress: '',
fiatAmount: 0,
fee: 512,
Expand All @@ -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 => {
Expand Down
189 changes: 87 additions & 102 deletions src/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,6 +11,7 @@ import { getDefaultSendTransaction } from '../shapes';
import {
EAddressType,
EBoostType,
ECoinSelect,
EFeeId,
IAddInput,
IAddresses,
Expand Down Expand Up @@ -71,7 +72,8 @@ export class Transaction {
utxos,
rbf = false,
satsPerByte = 1,
outputs
outputs,
coinselect = ECoinSelect.default
}: ISetupTransaction = {}): Promise<TSetupTransactionResponse> {
try {
const addressType = this._wallet.addressType;
Expand All @@ -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.');
Expand Down Expand Up @@ -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 = {
Expand All @@ -181,13 +173,14 @@ export class Transaction {
}
}

updateCoinselect = ({
recalculate = ({
transaction = this.data,
satsPerByte = this._data.satsPerByte
}: {
transaction?: ISendTransaction;
satsPerByte?: number;
}): Result<ISendTransaction> => {
const transaction = this._data;
const { max } = transaction;
const { availableInputs, coinselect } = transaction;

try {
const targets = transaction.outputs.map((output) => {
Expand All @@ -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<typeof cs> = undefined;

let selection: ReturnType<typeof coinselect> = 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.');
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/types/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
15 changes: 8 additions & 7 deletions src/types/wallet.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tests/send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down

0 comments on commit 108fa46

Please sign in to comment.