export type BCHUnits = 'sats' | 'bits' | 'mBCH' | 'BCH'; /** * Satoshis Primitive * * @example * * ```ts * // Import our primitives * import { Satoshis } from '@xocash/primitives'; * * // Create an amount from BCH. * const sats = Satoshis.fromBCH(0.54321); * * // Output as a satoshi number. * console.log(satoshis.toSats()); * * // Output as a satoshis bigint. * console.log(satoshis.toBigInt()); * ``` */ export class Satoshis { protected readonly satoshis: bigint; /** * Construct a new instance of Satoshis. * * This is the same as using fromSatoshis(). * * @param satoshis {number} The satoshi amount. */ protected constructor(satoshis: bigint | number) { // Set the satoshi amount. this.satoshis = BigInt(satoshis); } /** * Instantiates from a Satoshi Amount. * * @param satoshis {number} The Satoshi amount. * * @returns {Satoshis} New instance of Satoshis. */ public static fromSats(satoshis: bigint | number): Satoshis { return new this(satoshis); } /** * Instantiates from a Satoshi Amount. * * @param satoshis {number} The Satoshi amount. * * @deprecated Use fromSats() instead. * * @returns {Satoshis} New instance of Satoshis. */ public static fromSatoshis(satoshis: bigint | number): Satoshis { return new this(satoshis); } /** * Instantiates from a Bits Amount (100 satoshis = 1 bit). * * @param bits {number} The Bits amount. * * @returns {Satoshis} New instance of Satoshis. */ public static fromBits(bits: number): Satoshis { const satoshis = Satoshis.roundToDigits(bits * 100, 0); return new this(satoshis); } /** * Instantiates from a mBCH Amount (100,000 satoshis = 1 mBCH). * * @param mBCH {number} The mBCH amount. * * @returns {Satoshis} New instance of Satoshis. */ public static fromMBCH(mBCH: number): Satoshis { const satoshis = Satoshis.roundToDigits(mBCH * 100_000, 0); return new this(satoshis); } /** * Instantiates from a BCH Amount (100,000,000 satoshis = 1 BCH). * * @param bch {number} The BCH amount. * * @returns {Satoshis} New instance of Satoshis. */ public static fromBCH(bch: number): Satoshis { const satoshis = Satoshis.roundToDigits(bch * 100_000_000, 0); return new this(satoshis); } /** * Creates an instance from serialized data. * * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. * * @param bytes - Serialized Primitive Data * * @returns A new instance of this primitive. */ public static fromRaw(sats: bigint) { return new this(sats); } /** * Rounds a number to the given number of digits. * * @param numberToRound {number} The number to round. * @param digits {number} The number of digits to round to. * * @returns {Number} The rounded number. */ protected static roundToDigits( numberToRound: number, digits: number, ): number { // Set the options of the Number Format object. const options: Intl.NumberFormatOptions = { style: 'decimal', minimumFractionDigits: digits, maximumFractionDigits: digits, useGrouping: false, numberingSystem: 'latn', }; // Create an instance of number format using above options. // NOTE: We force the locale to en-GB so that the number is formatted correctly (e.g. with a decimal, not a comma). const numberFormat = new Intl.NumberFormat('en-GB', options); // Format the number. const formattedAmount = numberFormat.format(numberToRound); // Return the formatted number. return Number(formattedAmount); } /** * Converts the amount to Satoshis (as a BigInt). * * @returns {bigint} The satoshi amount. */ public toBigInt(): bigint { return this.satoshis; } /** * Converts the amount to Satoshis. * * @returns {number} The satoshi amount. */ public toSats(): number { return Number(this.satoshis); } /** * Converts the amount to Satoshis. * * @deprecated Use toSats() instead. * * @returns {number} The satoshi amount. */ public toSatoshis(): number { return Number(this.satoshis); } /** * Converts the amount to Bits (100 satoshis = 1 bit). * * @returns {number} The bit amount. */ public toBits(): number { return Satoshis.roundToDigits(Number(this.satoshis) / 100, 2); } /** * Converts the amount to mBCH (100,000 satoshis = 1 mBCH). * * @returns {number} The mBCH amount. */ public toMBCH(): number { return Satoshis.roundToDigits(Number(this.satoshis) / 100_000, 5); } /** * Converts the amount to BCH (100,000,000 satoshis = 1 BCH). * * @returns {number} The BCH amount. */ public toBCH(): number { return Satoshis.roundToDigits(Number(this.satoshis) / 100_000_000, 8); } /** * Converts this primitive to a serializable raw object representation. * * @returns Serialzied primitive data. */ toRaw() { return this.satoshis; } /** * Formats the amount in the specified unit with formatting options. * * @param units - The unit to format as ('sats', 'bits', 'mBCH', 'BCH') * @param opts - Formatting options * @param opts.roundToDigits - Number of decimals to round to * @param opts.appendSuffix - Whether to append the unit suffix * @returns Formatted amount as string * * @example * // Format as BCH with 2 decimal places and suffix * satoshis.format('BCH', { roundToDigits: 2, appendSuffix: true }) // "0.12 BCH" * * @example * // Format as satoshis without suffix * satoshis.format('sats', { roundToDigits: 0, appendSuffix: false }) // "12345678" */ public format( units: BCHUnits, opts: { roundToDigits?: number; appendSuffix?: boolean; showApproximationSymbol?: boolean; } = {}, ): string { // Set default values if not provided const roundToDigits = opts.roundToDigits ?? 4; const appendSuffix = opts.appendSuffix ?? true; const showApproximationSymbol = opts.showApproximationSymbol ?? true; // Get the exact amount in the specified unit before rounding let exactAmount: number; // Use type assertion to help TypeScript understand this is a valid BCHUnits value switch (units) { case 'sats': exactAmount = this.toSats(); break; case 'bits': exactAmount = Number(this.satoshis) / 100; break; case 'mBCH': exactAmount = Number(this.satoshis) / 100_000; break; case 'BCH': exactAmount = Number(this.satoshis) / 100_000_000; break; default: throw new Error(`Unsupported unit: ${units}`); } // Format the amount with the specified number of decimal places const roundedAmount = Satoshis.roundToDigits(exactAmount, roundToDigits); // Check if rounding occurred const wasRounded = exactAmount !== roundedAmount; // Add approximation symbol if needed and enabled const prefix = wasRounded && showApproximationSymbol ? '~' : ''; // Return the formatted amount with prefix and/or suffix return appendSuffix ? `${prefix}${roundedAmount} ${units}` : `${prefix}${roundedAmount}`; } }