Skip to content

Commit

Permalink
feat: ensure BigNumber classes are not returned (#89)
Browse files Browse the repository at this point in the history
* feat: ensure BigNumber classes are not returned

* chore: update test to typescript

* chore: more deliverable cleanups
  • Loading branch information
Shadouts authored Aug 24, 2023
1 parent ac5f754 commit e60da88
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 293 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@
"build-types": "tsc -p tsconfig.json",
"build-types:watch": "tsc --watch",
"check-types": "tsc -p tsconfig.json --noEmit --emitDeclarationOnly false",
"compile": "babel --extensions .ts,.js --ignore '**/*.test.js' --ignore '**/*.test.ts' -d lib/ src/",
"compile:watch": "babel --extensions .ts,.js --watch --ignore '**/*.test.js' --ignore '**/*.test.ts' -d lib/ src/",
"compile": "npm run build-types && babel --extensions .ts,.js --ignore '**/types' --ignore '**/*.test.js' --ignore '**/*.test.ts' -d lib/ src/",
"compile:watch": "npm run build-types:watch && babel --extensions .ts,.js --watch --ignore '**/*.test.js' --ignore '**/types' --ignore '**/*.test.ts' -d lib/ src/",
"coverage": "jest --coverage",
"prepublish": "npm run compile",
"pretest": "npm run build-types && npm run compile",
"pretest": "npm run compile",
"prepare": "npm run compile && husky install",
"test": "jest src",
"test:watch": "jest --watch src",
Expand Down
45 changes: 19 additions & 26 deletions src/fees.test.js → src/fees.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import BigNumber from 'bignumber.js';
import BigNumber from "bignumber.js";

import {
validateFeeRate,
validateFeeRate,
validateFee,
estimateMultisigTransactionFee,
estimateMultisigTransactionFeeRate,
} from './fees';
import {P2SH} from "./p2sh";
import {P2SH_P2WSH} from "./p2sh_p2wsh";
import {P2WSH} from "./p2wsh";
} from "./fees";
import { P2SH } from "./p2sh";
import { P2SH_P2WSH } from "./p2sh_p2wsh";
import { P2WSH } from "./p2wsh";

describe("fees", () => {

describe("validateFeeRate", () => {

it("should return an error message for an unparseable fee rate", () => {
BigNumber.DEBUG = true;
expect(validateFeeRate(null)).toMatch(/invalid fee rate/i);
BigNumber.DEBUG = false;
});

it("should return an error message for an unparseable fee rate", () => {
expect(validateFeeRate('foo')).toMatch(/invalid fee rate/i);
expect(validateFeeRate("foo")).toMatch(/invalid fee rate/i);
});

it("should return an error message for a negative fee rate", () => {
Expand All @@ -35,14 +33,13 @@ describe("fees", () => {
it("should return an error message when the fee rate is too high", () => {
expect(validateFeeRate(10000)).toMatch(/too high/i);
});

it("return an empty string for an acceptable fee rate", () => {
expect(validateFeeRate(100)).toBe("");
});
});

describe("validateFee", () => {

it("should return an error message for an unparseable fee", () => {
// If BigNumber.DEBUG is set true then an error will be thrown if this BigNumber constructor receives an invalid value
// see https://mikemcl.github.io/bignumber.js/#debug
Expand All @@ -58,11 +55,11 @@ describe("fees", () => {
});

it("should return an error message for an unparseable fee", () => {
expect(validateFee('foo', 1000000)).toMatch(/invalid fee/i);
expect(validateFee("foo", 1000000)).toMatch(/invalid fee/i);
});

it("should return an error message for an unparseable total input amount", () => {
expect(validateFee(10000, 'foo')).toMatch(/invalid total input amount/i);
expect(validateFee(10000, "foo")).toMatch(/invalid total input amount/i);
});

it("should return an error message for a negative fee", () => {
Expand Down Expand Up @@ -92,18 +89,16 @@ describe("fees", () => {
it("should return an empty string for an acceptable fee", () => {
expect(validateFee(10000, 1000000)).toBe("");
});

});

describe("estimating multisig transaction fees and fee rates", () => {

it("should estimate null for bad addressType", () => {
const params = {
addressType: 'foo',
addressType: "foo",
feesPerByteInSatoshis: "10",
};
const fee = estimateMultisigTransactionFee(params);
expect(isNaN(fee)).toBe(true);
expect(fee).toBe(null);
});

it("should estimate for P2SH transactions", () => {
Expand All @@ -118,8 +113,8 @@ describe("fees", () => {
};
const fee = estimateMultisigTransactionFee(params);
const feeRate = estimateMultisigTransactionFeeRate(params);
expect(fee).toEqual(BigNumber(params.feesInSatoshis));
expect(feeRate).toEqual(BigNumber(params.feesPerByteInSatoshis));
expect(fee).toEqual(String(params.feesInSatoshis));
expect(feeRate).toEqual(String(params.feesPerByteInSatoshis));
});

it("should estimate for P2SH-P2WSH transactions", () => {
Expand All @@ -134,8 +129,8 @@ describe("fees", () => {
};
const fee = estimateMultisigTransactionFee(params);
const feeRate = estimateMultisigTransactionFeeRate(params);
expect(fee).toEqual(BigNumber(params.feesInSatoshis));
expect(feeRate).toEqual(BigNumber(params.feesPerByteInSatoshis));
expect(fee).toEqual(String(params.feesInSatoshis));
expect(feeRate).toEqual(String(params.feesPerByteInSatoshis));
});

it("should estimate for P2WSH transactions", () => {
Expand All @@ -148,14 +143,12 @@ describe("fees", () => {
m: 2,
n: 3,
feesInSatoshis: "4550",
feesPerByteInSatoshis: "10"
feesPerByteInSatoshis: "10",
};
const fee = estimateMultisigTransactionFee(params);
const feeRate = estimateMultisigTransactionFeeRate(params);
expect(fee).toEqual(BigNumber(params.feesInSatoshis));
expect(feeRate).toEqual(BigNumber(params.feesPerByteInSatoshis));
expect(fee).toEqual(String(params.feesInSatoshis));
expect(feeRate).toEqual(String(params.feesPerByteInSatoshis));
});

});

});
117 changes: 65 additions & 52 deletions src/fees.js → src/fees.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
/**
/**
* This module provides functions for calculating & validating
* transaction fees.
*
*
* @module fees
*/

import BigNumber from 'bignumber.js';
import BigNumber from "bignumber.js";

import {
P2SH,
estimateMultisigP2SHTransactionVSize,
} from "./p2sh";
import { P2SH, estimateMultisigP2SHTransactionVSize } from "./p2sh";
import {
P2SH_P2WSH,
estimateMultisigP2SH_P2WSHTransactionVSize,
} from "./p2sh_p2wsh";
import {
P2WSH,
estimateMultisigP2WSHTransactionVSize,
} from "./p2wsh";
import {ZERO} from "./utils";
import { P2WSH, estimateMultisigP2WSHTransactionVSize } from "./p2wsh";
import { ZERO } from "./utils";

// Without this, BigNumber will report strings as exponentials. 16 places covers
// all possible values in satoshis.
BigNumber.config({ EXPONENTIAL_AT: 16 });

/**
* Maxmium acceptable transaction fee rate in Satoshis/vbyte.
*
* @constant
* @type {BigNumber}
* @default 1000 Satoshis/vbyte
*
*
*/
const MAX_FEE_RATE_SATS_PER_VBYTE = BigNumber(1000); // 1000 Sats/vbyte
const MAX_FEE_RATE_SATS_PER_VBYTE = new BigNumber(1000); // 1000 Sats/vbyte

/**
* Maxmium acceptable transaction fee in Satoshis.
Expand All @@ -38,18 +36,18 @@ const MAX_FEE_RATE_SATS_PER_VBYTE = BigNumber(1000); // 1000 Sats/vbyte
* @type {BigNumber}
* @default 2500000 Satoshis (=0.025 BTC)
*/
const MAX_FEE_SATS = BigNumber(2500000); // ~ 0.025 BTC ~ $250 if 1 BTC = $10k
const MAX_FEE_SATS = new BigNumber(2500000); // ~ 0.025 BTC ~ $250 if 1 BTC = $10k

/**
* Validate the given transaction fee rate (in Satoshis/vbyte).
*
* - Must be a parseable as a number.
*
* - Cannot be negative (zero is OK).
*
*
* - Cannot be greater than the limit set by
* `MAX_FEE_RATE_SATS_PER_VBYTE`.
*
*
* @param {string|number|BigNumber} feeRateSatsPerVbyte - the fee rate in Satoshis/vbyte
* @returns {string} empty if valid or corresponding validation message if not
* @example
Expand All @@ -61,8 +59,8 @@ const MAX_FEE_SATS = BigNumber(2500000); // ~ 0.025 BTC ~ $250 if 1 BTC = $10k
export function validateFeeRate(feeRateSatsPerVbyte) {
let fr;
try {
fr = BigNumber(feeRateSatsPerVbyte);
} catch(e) {
fr = new BigNumber(feeRateSatsPerVbyte);
} catch (e) {
return "Invalid fee rate.";
}
if (!fr.isFinite()) {
Expand All @@ -74,7 +72,7 @@ export function validateFeeRate(feeRateSatsPerVbyte) {
if (fr.isGreaterThan(MAX_FEE_RATE_SATS_PER_VBYTE)) {
return "Fee rate is too high.";
}
return '';
return "";
}

/**
Expand All @@ -87,7 +85,7 @@ export function validateFeeRate(feeRateSatsPerVbyte) {
* - Cannot exceed the total input amount.
*
* - Cannot be higher than the limit set by `MAX_FEE_SATS`.
*
*
* @param {string|number|BigNumber} feeSats - fee in Satoshis
* @param {string|number|BigNumber} inputsTotalSats - total input amount in Satoshis
* @returns {string} empty if valid or corresponding validation message if not
Expand All @@ -101,16 +99,16 @@ export function validateFeeRate(feeRateSatsPerVbyte) {
export function validateFee(feeSats, inputsTotalSats) {
let fs, its;
try {
fs = BigNumber(feeSats);
} catch(e) {
fs = new BigNumber(feeSats);
} catch (e) {
return "Invalid fee.";
}
if (!fs.isFinite()) {
return "Invalid fee.";
}
try {
its = BigNumber(inputsTotalSats);
} catch(e) {
its = new BigNumber(inputsTotalSats);
} catch (e) {
return "Invalid total input amount.";
}
if (!its.isFinite()) {
Expand All @@ -128,39 +126,42 @@ export function validateFee(feeSats, inputsTotalSats) {
if (fs.isGreaterThan(MAX_FEE_SATS)) {
return "Fee is too high.";
}
return '';
return "";
}


/**
* Estimate transaction fee rate based on actual fee and address type, number of inputs and number of outputs.
*
*
* @param {Object} config - configuration for the calculation
* @param {module:multisig.MULTISIG_ADDRESS_TYPES} config.addressType - address type used for estimation
* @param {number} config.numInputs - number of inputs used in calculation
* @param {number} config.numOutputs - number of outputs used in calculation
* @param {number} config.m - number of required signers for the quorum
* @param {number} config.n - number of total signers for the quorum
* @param {BigNumber} config.feesInSatoshis - total transaction fee in satoshis
* @example
* @example
* import {estimateMultisigP2WSHTransactionFeeRate} from "unchained-bitcoin";
* // get the fee rate a P2WSH multisig transaction with 2 inputs and 3 outputs with a known fee of 7060
* const feerate = estimateMultisigTransactionFeeRate({
* addressType: P2WSH,
* numInputs: 2,
* numOutputs: 3,
* addressType: P2WSH,
* numInputs: 2,
* numOutputs: 3,
* m: 2,
* n: 3,
* feesInSatoshis: 7060
* });
*
*
* @returns {string} estimated fee rate
*
*
* @returns {string|null} estimated fee rate or null if vSize is null
*/
export function estimateMultisigTransactionFeeRate(config) {
return (BigNumber(config.feesInSatoshis)).dividedBy(
estimateMultisigTransactionVSize(config)
);
const vSize = estimateMultisigTransactionVSize(config);

if (vSize === null) {
return null;
}

return new BigNumber(config.feesInSatoshis).dividedBy(vSize).toString();
}

/**
Expand All @@ -176,29 +177,41 @@ export function estimateMultisigTransactionFeeRate(config) {
* // get fee for P2SH multisig transaction with 2 inputs and 3 outputs at 10 satoshis per byte
* import {estimateMultisigP2WSHTransactionFee} from "unchained-bitcoin";
* const fee = estimateMultisigTransactionFee({
* addressType: P2SH,
* numInputs: 2,
* numOutputs: 3,
* addressType: P2SH,
* numInputs: 2,
* numOutputs: 3,
* m: 2,
* n: 3,
* feesPerByteInSatoshis: 10
* });
* @returns {number} estimated transaction fee
* @returns {string|null} estimated transaction fee in satoshis or null if vSize is null
*/
export function estimateMultisigTransactionFee(config) {
return (BigNumber(config.feesPerByteInSatoshis)).multipliedBy(
estimateMultisigTransactionVSize(config));
const vSize = estimateMultisigTransactionVSize(config);

if (vSize === null) {
return null;
}

const feeAsNumber = new BigNumber(config.feesPerByteInSatoshis)
.multipliedBy(vSize)
.toNumber();

// In the case that feesPerByteInSatoshis is a float, feeAsNumber might be a
// float. A fraction of a satoshi is not possible on-chain. Estimate worse
// case fee and calculate ceil.
return Math.ceil(feeAsNumber).toString();
}

function estimateMultisigTransactionVSize(config) {
switch (config.addressType) {
case P2SH:
return estimateMultisigP2SHTransactionVSize(config);
case P2SH_P2WSH:
return estimateMultisigP2SH_P2WSHTransactionVSize(config);
case P2WSH:
return estimateMultisigP2WSHTransactionVSize(config);
default:
return null;
case P2SH:
return estimateMultisigP2SHTransactionVSize(config);
case P2SH_P2WSH:
return estimateMultisigP2SH_P2WSHTransactionVSize(config);
case P2WSH:
return estimateMultisigP2WSHTransactionVSize(config);
default:
return null;
}
}
Loading

0 comments on commit e60da88

Please sign in to comment.