diff --git a/src/fees.test.ts b/src/fees.test.ts index d546b6a..205fa6a 100644 --- a/src/fees.test.ts +++ b/src/fees.test.ts @@ -1,10 +1,13 @@ import BigNumber from "bignumber.js"; +import { FeeValidationError } from "./types"; import { validateFeeRate, validateFee, estimateMultisigTransactionFee, estimateMultisigTransactionFeeRate, + checkFeeError, + checkFeeRateError, } from "./fees"; import { P2SH } from "./p2sh"; import { P2SH_P2WSH } from "./p2sh_p2wsh"; @@ -12,82 +15,165 @@ import { P2WSH } from "./p2wsh"; describe("fees", () => { describe("validateFeeRate", () => { - it("should return an error message for an unparseable fee rate", () => { + it("should return an error and message for an unparseable fee rate", () => { BigNumber.DEBUG = true; - expect(validateFeeRate(null)).toMatch(/invalid fee rate/i); + const feeRateSatsPerVbyte = null; + expect(checkFeeRateError(feeRateSatsPerVbyte)).toBe( + FeeValidationError.INVALID_FEE_RATE + ); + expect(validateFeeRate(feeRateSatsPerVbyte)).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); + it("should return an error and message for an unparseable fee rate", () => { + const feeRateSatsPerVbyte = "foo"; + expect(checkFeeRateError(feeRateSatsPerVbyte)).toBe( + FeeValidationError.INVALID_FEE_RATE + ); + expect(validateFeeRate(feeRateSatsPerVbyte)).toMatch(/invalid fee rate/i); }); - it("should return an error message for a negative fee rate", () => { - expect(validateFeeRate(-1)).toMatch(/cannot be negative/i); + it("should return an error and message for a negative fee rate", () => { + const feeRateSatsPerVbyte = -1; + expect(checkFeeRateError(feeRateSatsPerVbyte)).toBe( + FeeValidationError.FEE_RATE_CANNOT_BE_NEGATIVE + ); + expect(validateFeeRate(feeRateSatsPerVbyte)).toMatch( + /cannot be negative/i + ); }); it("should return an empty string for a zero fee rate", () => { - expect(validateFeeRate(0)).toBe(""); + const feeRateSatsPerVbyte = 0; + expect(checkFeeRateError(feeRateSatsPerVbyte)).toBe(null); + expect(validateFeeRate(feeRateSatsPerVbyte)).toBe(""); }); - it("should return an error message when the fee rate is too high", () => { - expect(validateFeeRate(10000)).toMatch(/too high/i); + it("should return an error and message when the fee rate is too high", () => { + const feeRateSatsPerVbyte = 10000; + expect(checkFeeRateError(feeRateSatsPerVbyte)).toBe( + FeeValidationError.FEE_RATE_TOO_HIGH + ); + expect(validateFeeRate(feeRateSatsPerVbyte)).toMatch(/too high/i); }); - it("return an empty string for an acceptable fee rate", () => { - expect(validateFeeRate(100)).toBe(""); + it("return an empty string and no error for an acceptable fee rate", () => { + const feeRateSatsPerVbyte = 100; + expect(checkFeeRateError(feeRateSatsPerVbyte)).toBe(null); + expect(validateFeeRate(feeRateSatsPerVbyte)).toBe(""); }); }); describe("validateFee", () => { - it("should return an error message for an unparseable fee", () => { + it("should return an error and 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 BigNumber.DEBUG = true; - expect(validateFee(null, 1000000)).toMatch(/invalid fee/i); + const feeSats = null; + const inputsTotalSats = 1000000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.INVALID_FEE + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch(/invalid fee/i); BigNumber.DEBUG = false; }); - it("should return an error message for an unparseable inputTotalSats", () => { + it("should return an error and message for an unparseable inputTotalSats", () => { BigNumber.DEBUG = true; - expect(validateFee(10000, null)).toMatch(/invalid total input amount/i); + const feeSats = 10000; + const inputsTotalSats = null; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.INVALID_INPUT_AMOUNT + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch( + /invalid total input amount/i + ); BigNumber.DEBUG = false; }); - it("should return an error message for an unparseable fee", () => { - 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); - }); - - it("should return an error message for a negative fee", () => { - expect(validateFee(-1, 1000000)).toMatch(/cannot be negative/i); - }); - - it("should return an error message for a negative total input amount", () => { - expect(validateFee(10000, -1)).toMatch(/must be positive/i); - }); - - it("should return an error message for a zero total linput amount", () => { - expect(validateFee(10000, 0)).toMatch(/must be positive/i); - }); - - it("should return an empty string for a zero fee", () => { - expect(validateFee(0, 1000000)).toBe(""); - }); - - it("should return an error message when the fee is too high", () => { - expect(validateFee(2500001, 10000000)).toMatch(/too high/i); - }); - - it("should return an error message when the fee higher than the total input amount", () => { - expect(validateFee(100001, 100000)).toMatch(/too high/i); - }); - - it("should return an empty string for an acceptable fee", () => { - expect(validateFee(10000, 1000000)).toBe(""); + it("should return an error and message for an unparseable fee", () => { + const feeSats = "foo"; + const inputsTotalSats = 1000000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.INVALID_FEE + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch(/invalid fee/i); + }); + + it("should return an error and message for an unparseable total input amount", () => { + const feeSats = 10000; + const inputsTotalSats = "foo"; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.INVALID_INPUT_AMOUNT + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch( + /invalid total input amount/i + ); + }); + + it("should return an error and message for a negative fee", () => { + const feeSats = -1; + const inputsTotalSats = 1000000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.FEE_CANNOT_BE_NEGATIVE + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch( + /cannot be negative/i + ); + }); + + it("should return an error and message for a negative total input amount", () => { + const feeSats = 10000; + const inputsTotalSats = -1; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.INPUT_AMOUNT_MUST_BE_POSITIVE + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch( + /must be positive/i + ); + }); + + it("should return an error and message for a zero total linput amount", () => { + const feeSats = 10000; + const inputsTotalSats = 0; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.INPUT_AMOUNT_MUST_BE_POSITIVE + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch( + /must be positive/i + ); + }); + + it("should return an empty string and not error for a zero fee", () => { + const feeSats = 0; + const inputsTotalSats = 1000000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe(null); + expect(validateFee(feeSats, inputsTotalSats)).toBe(""); + }); + + it("should return an error and message when the fee is too high", () => { + const feeSats = 2500001; + const inputsTotalSats = 10000000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.FEE_TOO_HIGH + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch(/too high/i); + }); + + it("should return an error and message when the fee higher than the total input amount", () => { + const feeSats = 100001; + const inputsTotalSats = 100000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe( + FeeValidationError.FEE_TOO_HIGH + ); + expect(validateFee(feeSats, inputsTotalSats)).toMatch(/too high/i); + }); + + it("should return an empty string and no error for an acceptable fee", () => { + const feeSats = 10000; + const inputsTotalSats = 1000000; + expect(checkFeeError(feeSats, inputsTotalSats)).toBe(null); + expect(validateFee(feeSats, inputsTotalSats)).toBe(""); }); }); diff --git a/src/fees.ts b/src/fees.ts index ee31cc5..69c0e6b 100644 --- a/src/fees.ts +++ b/src/fees.ts @@ -5,6 +5,8 @@ import BigNumber from "bignumber.js"; +import { FeeValidationError } from "./types"; + import { P2SH, estimateMultisigP2SHTransactionVSize } from "./p2sh"; import { P2SH_P2WSH, @@ -28,76 +30,141 @@ const MAX_FEE_RATE_SATS_PER_VBYTE = new BigNumber(1000); // 1000 Sats/vbyte 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`. + * Provide a readable message from a FeeValidationError for user display. */ -export function validateFeeRate(feeRateSatsPerVbyte) { - let fr; - try { - fr = new BigNumber(feeRateSatsPerVbyte); - } catch (e) { - return "Invalid fee rate."; - } - if (!fr.isFinite()) { - return "Invalid fee rate."; - } - if (fr.isLessThan(ZERO)) { - return "Fee rate cannot be negative."; - } - if (fr.isGreaterThan(MAX_FEE_RATE_SATS_PER_VBYTE)) { - return "Fee rate is too high."; +export function getFeeErrorMessage(error: FeeValidationError | null) { + let errorMessage = ""; + + switch (error) { + case FeeValidationError.FEE_CANNOT_BE_NEGATIVE: + errorMessage = "Fee cannot be negative."; + break; + + case FeeValidationError.FEE_RATE_CANNOT_BE_NEGATIVE: + errorMessage = "Fee rate cannot be negative."; + break; + + case FeeValidationError.FEE_TOO_HIGH: + errorMessage = "Fee is too high."; + break; + + case FeeValidationError.FEE_RATE_TOO_HIGH: + errorMessage = "Fee rate is too high."; + break; + + case FeeValidationError.INPUT_AMOUNT_MUST_BE_POSITIVE: + errorMessage = "Total input amount must be positive."; + break; + + case FeeValidationError.INVALID_FEE: + errorMessage = "Invalid fee."; + break; + + case FeeValidationError.INVALID_FEE_RATE: + errorMessage = "Invalid fee rate."; + break; + + case FeeValidationError.INVALID_INPUT_AMOUNT: + errorMessage = "Invalid total input amount."; + break; + + default: + break; } - return ""; + + return errorMessage; } /** - * Validate the given transaction fee (in Satoshis). - * - * - Must be a parseable as a number. - * - * - Cannot be negative (zero is OK). - * - * - Cannot exceed the total input amount. - * - * - Cannot be higher than the limit set by `MAX_FEE_SATS`. + * Validate the given transaction fee and input sats. Returns a fee + * validation error type if invalid. Returns null if valid. */ -export function validateFee(feeSats, inputsTotalSats) { +export function checkFeeError(feeSats, inputsTotalSats) { let fs, its; try { fs = new BigNumber(feeSats); } catch (e) { - return "Invalid fee."; + return FeeValidationError.INVALID_FEE; } if (!fs.isFinite()) { - return "Invalid fee."; + return FeeValidationError.INVALID_FEE; } try { its = new BigNumber(inputsTotalSats); } catch (e) { - return "Invalid total input amount."; + return FeeValidationError.INVALID_INPUT_AMOUNT; } if (!its.isFinite()) { - return "Invalid total input amount."; + return FeeValidationError.INVALID_INPUT_AMOUNT; } if (fs.isLessThan(ZERO)) { - return "Fee cannot be negative."; + return FeeValidationError.FEE_CANNOT_BE_NEGATIVE; } if (its.isLessThanOrEqualTo(ZERO)) { - return "Total input amount must be positive."; + return FeeValidationError.INPUT_AMOUNT_MUST_BE_POSITIVE; } if (fs.isGreaterThan(its)) { - return "Fee is too high."; + return FeeValidationError.FEE_TOO_HIGH; } if (fs.isGreaterThan(MAX_FEE_SATS)) { - return "Fee is too high."; + return FeeValidationError.FEE_TOO_HIGH; } - return ""; + return null; +} + +/** + * Validate the given transaction fee rate (sats/vByte). Returns a fee + * validation error type if invalid. Returns null if valid. + */ +export function checkFeeRateError(feeRateSatsPerVbyte) { + let fr: BigNumber; + + try { + fr = new BigNumber(feeRateSatsPerVbyte); + } catch (e) { + return FeeValidationError.INVALID_FEE_RATE; + } + if (!fr.isFinite()) { + return FeeValidationError.INVALID_FEE_RATE; + } + if (fr.isLessThan(ZERO)) { + return FeeValidationError.FEE_RATE_CANNOT_BE_NEGATIVE; + } + if (fr.isGreaterThan(MAX_FEE_RATE_SATS_PER_VBYTE)) { + return FeeValidationError.FEE_RATE_TOO_HIGH; + } + + return null; +} + +/** + * Validate the given transaction fee rate (in Satoshis/vbyte). Returns an + * error message if invalid. Returns empty string if valid. + * + * - 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`. + */ +export function validateFeeRate(feeRateSatsPerVbyte) { + return getFeeErrorMessage(checkFeeRateError(feeRateSatsPerVbyte)); +} + +/** + * Validate the given transaction fee (in Satoshis). + * + * - Must be a parseable as a number. + * + * - Cannot be negative (zero is OK). + * + * - Cannot exceed the total input amount. + * + * - Cannot be higher than the limit set by `MAX_FEE_SATS`. + */ +export function validateFee(feeSats, inputsTotalSats) { + return getFeeErrorMessage(checkFeeError(feeSats, inputsTotalSats)); } /** diff --git a/src/types/fees.ts b/src/types/fees.ts new file mode 100644 index 0000000..c6e682d --- /dev/null +++ b/src/types/fees.ts @@ -0,0 +1,11 @@ +// eslint-disable-next-line no-shadow +export enum FeeValidationError { + FEE_CANNOT_BE_NEGATIVE, + FEE_RATE_CANNOT_BE_NEGATIVE, + FEE_TOO_HIGH, + FEE_RATE_TOO_HIGH, + INPUT_AMOUNT_MUST_BE_POSITIVE, + INVALID_FEE, + INVALID_FEE_RATE, + INVALID_INPUT_AMOUNT, +} diff --git a/src/types/index.ts b/src/types/index.ts index 5482d17..a92ece5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,3 +2,4 @@ export * from "./keys"; export * from "./addresses"; export * from "./networks"; export * from "./braid"; +export * from "./fees";