import { NumberSerializer, Serializer, mapSerializer, } from '@metaplex-foundation/umi-serializers'; import { BigIntInput, createBigInt } from './BigInt'; import { AmountMismatchError, UnexpectedAmountError } from './errors'; /** * The identifier of an amount. * @category Utils — Amounts */ export type AmountIdentifier = 'SOL' | 'USD' | '%' | 'splToken' | string; /** * The number of decimals in an amount represented using the lowest possible unit. * @category Utils — Amounts */ export type AmountDecimals = number; /** * Describes an amount of any type or currency using the lowest possible unit. * It uses a BigInt to represent the basis points of the amount, a decimal number * to know how to interpret the basis points, and an identifier to know what * type of amount we are dealing with. * * Custom type parameters can be used to represent specific types of amounts. * For example: * - Amount<'SOL', 9> represents an amount of SOL in lamports. * - Amount<'USD', 2> represents an amount of USD in cents. * - Amount<'%', 2> represents a percentage with 2 decimals. * * @category Utils — Amounts */ export type Amount< I extends AmountIdentifier = AmountIdentifier, D extends AmountDecimals = AmountDecimals > = { /** The amount in its lower possible unit such that it does not contain decimals. */ basisPoints: bigint; /** The identifier of the amount. */ identifier: I; /** The number of decimals in the amount. */ decimals: D; }; /** * An amount of SOL represented using the lowest possible unit — i.e. lamports. * @category Utils — Amounts */ export type SolAmount = Amount<'SOL', 9>; /** * An amount of US dollars represented using the lowest possible unit — i.e. cents. * @category Utils — Amounts */ export type UsdAmount = Amount<'USD', 2>; /** * An amount of SOL represented using the micro units — i.e. microlamports. * @category Utils — Amounts */ export type MicroSolAmount = Amount<'uSOL', 15>; /** * An percentage represented in basis points using a given number of decimals. * @category Utils — Amounts */ export type PercentAmount = Amount<'%', D>; /** * Creates an amount from the provided basis points, identifier, and decimals. * @category Utils — Amounts */ export const createAmount = < I extends AmountIdentifier, D extends AmountDecimals >( basisPoints: BigIntInput, identifier: I, decimals: D ): Amount => ({ basisPoints: createBigInt(basisPoints), identifier, decimals, }); /** * Creates an amount from a decimal value which will be converted to the lowest * possible unit using the provided decimals. * @category Utils — Amounts */ export const createAmountFromDecimals = < I extends AmountIdentifier, D extends AmountDecimals >( decimalAmount: number, identifier: I, decimals: D ): Amount => { const exponentAmount = createAmount( BigInt(10) ** BigInt(decimals ?? 0), identifier, decimals ); return multiplyAmount(exponentAmount, decimalAmount); }; /** * Creates a percentage amount from the provided decimal value. * @category Utils — Amounts */ export const percentAmount = ( percent: number, decimals: D = 2 as D ): Amount<'%', D> => createAmountFromDecimals(percent, '%', decimals); /** * Creates an amount of SPL tokens from the provided decimal value. * @category Utils — Amounts */ export const tokenAmount = < I extends AmountIdentifier, D extends AmountDecimals >( tokens: number, identifier?: I, decimals?: D ): Amount => createAmountFromDecimals( tokens, (identifier ?? 'splToken') as I, (decimals ?? 0) as D ); /** * Creates a {@link SolAmount} from the provided lamports. * @category Utils — Amounts */ export const lamports = (lamports: BigIntInput): SolAmount => createAmount(lamports, 'SOL', 9); /** * Creates a {@link SolAmount} from the provided decimal value in SOL. * @category Utils — Amounts */ export const sol = (sol: number): SolAmount => createAmountFromDecimals(sol, 'SOL', 9); /** * Creates a {@link UsdAmount} from the provided decimal value in USD. * @category Utils — Amounts */ export const usd = (usd: number): UsdAmount => createAmountFromDecimals(usd, 'USD', 2); /** * Creates a {@link MicroSolAmount} from the provided micro lamports. * @category Utils — Amounts */ export const microLamports = (microLamports: BigIntInput): MicroSolAmount => createAmount(microLamports, 'uSOL', 15); /** * Creates a {@link MicroSolAmount} from the provided decimal value in SOL. * @category Utils — Amounts */ export const microSol = (sol: number): MicroSolAmount => createAmountFromDecimals(sol, 'uSOL', 15); /** * Determines whether a given amount has the provided identifier and decimals. * @category Utils — Amounts */ export const isAmount = ( amount: Amount, identifier: I, decimals: D ): amount is Amount => amount.identifier === identifier && amount.decimals === decimals; /** * Determines whether a given amount is a {@link SolAmount}. * @category Utils — Amounts */ export const isSolAmount = (amount: Amount): amount is SolAmount => isAmount(amount, 'SOL', 9); /** * Determines whether two amounts are of the same type. * @category Utils — Amounts */ export const sameAmounts = (left: Amount, right: Amount): boolean => isAmount(left, right.identifier, right.decimals); /** * Ensures that a given amount has the provided identifier and decimals. * @category Utils — Amounts */ export function assertAmount< I extends AmountIdentifier, D extends AmountDecimals >(amount: Amount, identifier: I, decimals: D): asserts amount is Amount { if (!isAmount(amount, identifier, decimals)) { throw new UnexpectedAmountError(amount, identifier, decimals); } } /** * Ensures that a given amount is a {@link SolAmount}. * @category Utils — Amounts */ export function assertSolAmount(actual: Amount): asserts actual is SolAmount { assertAmount(actual, 'SOL', 9); } /** * Ensures that two amounts are of the same type. * @category Utils — Amounts */ export function assertSameAmounts( left: Amount, right: Amount, operation?: string ) { if (!sameAmounts(left, right)) { throw new AmountMismatchError(left, right, operation); } } /** * Adds two amounts of the same type. * @category Utils — Amounts */ export const addAmounts = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): Amount => { assertSameAmounts(left, right, 'add'); return { ...left, basisPoints: left.basisPoints + right.basisPoints, }; }; /** * Subtracts two amounts of the same type. * @category Utils — Amounts */ export const subtractAmounts = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): Amount => { assertSameAmounts(left, right, 'subtract'); return { ...left, basisPoints: left.basisPoints - right.basisPoints, }; }; /** * Multiplies an amount by a given multiplier. * @category Utils — Amounts */ export const multiplyAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, multiplier: number | bigint ): Amount => { if (typeof multiplier === 'bigint') { return { ...left, basisPoints: left.basisPoints * multiplier }; } const [units, decimals] = multiplier.toString().split('.'); const multiplierBasisPoints = BigInt(units + (decimals ?? '')); const multiplierExponents = BigInt(10) ** BigInt(decimals?.length ?? 0); return { ...left, basisPoints: (left.basisPoints * multiplierBasisPoints) / multiplierExponents, }; }; /** * Divides an amount by a given divisor. * @category Utils — Amounts */ export const divideAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, divisor: number | bigint ): Amount => { if (typeof divisor === 'bigint') { return { ...left, basisPoints: left.basisPoints / divisor }; } const [units, decimals] = divisor.toString().split('.'); const divisorBasisPoints = BigInt(units + (decimals ?? '')); const divisorExponents = BigInt(10) ** BigInt(decimals?.length ?? 0); return { ...left, basisPoints: (left.basisPoints * divisorExponents) / divisorBasisPoints, }; }; /** * Returns the absolute value of an amount. * @category Utils — Amounts */ export const absoluteAmount = < I extends AmountIdentifier, D extends AmountDecimals >( value: Amount ): Amount => { const x = value.basisPoints; return { ...value, basisPoints: x < 0 ? -x : x }; }; /** * Compares two amounts of the same type. * @category Utils — Amounts */ export const compareAmounts = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): -1 | 0 | 1 => { assertSameAmounts(left, right, 'compare'); if (left.basisPoints > right.basisPoints) return 1; if (left.basisPoints < right.basisPoints) return -1; return 0; }; /** * Determines whether two amounts are equal. * An optional tolerance can be provided to allow for small differences. * When using {@link SolAmount}, this is usually due to transaction or small storage fees. * @category Utils — Amounts */ export const isEqualToAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount, tolerance?: Amount ): boolean => { tolerance = tolerance ?? createAmount(0, left.identifier, left.decimals); assertSameAmounts(left, right, 'isEqualToAmount'); assertSameAmounts(left, tolerance, 'isEqualToAmount'); const delta = absoluteAmount(subtractAmounts(left, right)); return isLessThanOrEqualToAmount(delta, tolerance); }; /** * Whether the left amount is less than the right amount. * @category Utils — Amounts */ export const isLessThanAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): boolean => compareAmounts(left, right) < 0; /** * Whether the left amount is less than or equal to the right amount. * @category Utils — Amounts */ export const isLessThanOrEqualToAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): boolean => compareAmounts(left, right) <= 0; /** * Whether the left amount is greater than the right amount. * @category Utils — Amounts */ export const isGreaterThanAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): boolean => compareAmounts(left, right) > 0; /** * Whether the left amount is greater than or equal to the right amount. * @category Utils — Amounts */ export const isGreaterThanOrEqualToAmount = < I extends AmountIdentifier, D extends AmountDecimals >( left: Amount, right: Amount ): boolean => compareAmounts(left, right) >= 0; /** * Whether the amount is zero. * @category Utils — Amounts */ export const isZeroAmount = (value: Amount): boolean => value.basisPoints === BigInt(0); /** * Whether the amount is positive. * @category Utils — Amounts */ export const isPositiveAmount = (value: Amount): boolean => value.basisPoints >= BigInt(0); /** * Whether the amount is negative. * @category Utils — Amounts */ export const isNegativeAmount = (value: Amount): boolean => value.basisPoints < BigInt(0); /** * Converts an amount to a string by using the amount's decimals. * @category Utils — Amounts */ export const amountToString = (value: Amount, maxDecimals?: number): string => { let text = value.basisPoints.toString(); if (value.decimals === 0) { return text; } const sign = text.startsWith('-') ? '-' : ''; text = text.replace('-', ''); text = text.padStart(value.decimals + 1, '0'); const units = text.slice(0, -value.decimals); let decimals = text.slice(-value.decimals); if (maxDecimals !== undefined) { decimals = decimals.slice(0, maxDecimals); } return `${sign + units}.${decimals}`; }; /** * Converts an amount to a number by using the amount's decimals. * Note that this may throw an error if the amount is too large to fit in a JavaScript number. * @category Utils — Amounts */ export const amountToNumber = (value: Amount): number => parseFloat(amountToString(value)); /** * Displays an amount as a string by using the amount's decimals and identifier. * @category Utils — Amounts */ export const displayAmount = (value: Amount, maxDecimals?: number): string => { const amountAsString = amountToString(value, maxDecimals); switch (value.identifier) { case '%': return `${amountAsString}%`; case 'splToken': return /^1(\.0+)?$/.test(amountAsString) ? `${amountAsString} Token` : `${amountAsString} Tokens`; default: if (value.identifier.startsWith('splToken.')) { const [, identifier] = value.identifier.split('.'); return `${identifier} ${amountAsString}`; } return `${value.identifier} ${amountAsString}`; } }; /** * Converts a number serializer into an amount serializer * by providing an amount identifier and decimals. * @category Utils — Amounts */ export const mapAmountSerializer = < I extends AmountIdentifier = AmountIdentifier, D extends AmountDecimals = AmountDecimals >( serializer: NumberSerializer, identifier: I, decimals: D ): Serializer> => mapSerializer( serializer as Serializer, (value: Amount): number | bigint => value.basisPoints > Number.MAX_SAFE_INTEGER ? value.basisPoints : Number(value.basisPoints), (value: number | bigint): Amount => createAmount(value, identifier, decimals) );