import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import BN from 'bn.js'; import { CurrencyMismatchError, UnexpectedCurrencyError } from '../errors'; import { BigNumber, BigNumberValues, toBigNumber } from './BigNumber'; export type Amount = { basisPoints: BigNumber; currency: T; }; export type Currency = { symbol: string; decimals: number; namespace?: 'spl-token'; }; export type SplTokenCurrency = { symbol: string; decimals: number; namespace: 'spl-token'; }; export type SplTokenAmount = Amount; /** @group Constants */ export const SOL = { symbol: 'SOL', decimals: 9, } as const; export type SolCurrency = typeof SOL; export type SolAmount = Amount; /** @group Constants */ export const USD = { symbol: 'USD', decimals: 2, } as const; export type UsdCurrency = typeof USD; export type UsdAmount = Amount; export const amount = ( basisPoints: BigNumberValues, currency: T ): Amount => { return { basisPoints: toBigNumber(basisPoints), currency, }; }; export const lamports = (lamports: BigNumberValues): SolAmount => { return amount(lamports, SOL); }; export const sol = (sol: number): SolAmount => { return lamports(sol * LAMPORTS_PER_SOL); }; export const usd = (usd: number): UsdAmount => { return amount(usd * 100, USD); }; export const token = ( amount: BigNumberValues, decimals = 0, symbol = 'Token' ): SplTokenAmount => { const basisPoints = toBigNumber( toBigNumber(amount).mul(toBigNumber(Math.pow(10, decimals))) ); return { basisPoints, currency: { symbol, decimals, namespace: 'spl-token', }, }; }; export const isSol = (currencyOrAmount: Currency | Amount): boolean => { return sameCurrencies(currencyOrAmount, SOL); }; export const sameAmounts = (left: Amount, right: Amount): boolean => { return sameCurrencies(left, right) && left.basisPoints.eq(right.basisPoints); }; export const sameCurrencies = ( left: Currency | Amount, right: Currency | Amount ): boolean => { if ('currency' in left) { left = left.currency; } if ('currency' in right) { right = right.currency; } return ( left.symbol === right.symbol && left.decimals === right.decimals && left.namespace === right.namespace ); }; export function assertCurrency( actual: Currency, expected: T ): asserts actual is T; export function assertCurrency( actual: Amount, expected: T ): asserts actual is Amount; export function assertCurrency( actual: Currency | Amount, expected: T ): asserts actual is T | Amount; export function assertCurrency( actual: Currency | Amount, expected: T ): asserts actual is T | Amount { if ('currency' in actual) { actual = actual.currency; } if (!sameCurrencies(actual, expected)) { throw new UnexpectedCurrencyError(actual, expected); } } export function assertSol(actual: Amount): asserts actual is SolAmount; export function assertSol(actual: Currency): asserts actual is SolCurrency; export function assertSol( actual: Currency | Amount ): asserts actual is SolCurrency | SolAmount; export function assertSol( actual: Currency | Amount ): asserts actual is SolCurrency | SolAmount { assertCurrency(actual, SOL); } export function assertSameCurrencies( left: L | Amount, right: R | Amount, operation?: string ) { if ('currency' in left) { left = left.currency; } if ('currency' in right) { right = right.currency; } if (!sameCurrencies(left, right)) { throw new CurrencyMismatchError(left, right, operation); } } export const addAmounts = ( left: Amount, right: Amount ): Amount => { assertSameCurrencies(left, right, 'add'); return amount(left.basisPoints.add(right.basisPoints), left.currency); }; export const subtractAmounts = ( left: Amount, right: Amount ): Amount => { assertSameCurrencies(left, right, 'subtract'); return amount(left.basisPoints.sub(right.basisPoints), left.currency); }; export const multiplyAmount = ( left: Amount, multiplier: number ): Amount => { return amount(left.basisPoints.muln(multiplier), left.currency); }; export const divideAmount = ( left: Amount, divisor: number ): Amount => { return amount(left.basisPoints.divn(divisor), left.currency); }; export const absoluteAmount = ( value: Amount ): Amount => { return amount(value.basisPoints.abs(), value.currency); }; export const compareAmounts = ( left: Amount, right: Amount ): -1 | 0 | 1 => { assertSameCurrencies(left, right, 'compare'); return left.basisPoints.cmp(right.basisPoints); }; export const isEqualToAmount = ( left: Amount, right: Amount, tolerance?: Amount ): boolean => { tolerance = tolerance ?? amount(0, left.currency); assertSameCurrencies(left, right, 'isEqualToAmount'); assertSameCurrencies(left, tolerance, 'isEqualToAmount'); const delta = absoluteAmount(subtractAmounts(left, right)); return isLessThanOrEqualToAmount(delta, tolerance); }; export const isLessThanAmount = ( left: Amount, right: Amount ): boolean => compareAmounts(left, right) < 0; export const isLessThanOrEqualToAmount = ( left: Amount, right: Amount ): boolean => compareAmounts(left, right) <= 0; export const isGreaterThanAmount = ( left: Amount, right: Amount ): boolean => compareAmounts(left, right) > 0; export const isGreaterThanOrEqualToAmount = ( left: Amount, right: Amount ): boolean => compareAmounts(left, right) >= 0; export const isZeroAmount = (value: Amount): boolean => compareAmounts(value, amount(0, value.currency)) === 0; export const isPositiveAmount = (value: Amount): boolean => compareAmounts(value, amount(0, value.currency)) >= 0; export const isNegativeAmount = (value: Amount): boolean => compareAmounts(value, amount(0, value.currency)) < 0; export const formatAmount = (value: Amount): string => { if (value.currency.decimals === 0) { return `${value.currency.symbol} ${value.basisPoints.toString()}`; } const power = new BN(10).pow(new BN(value.currency.decimals)); const basisPoints = value.basisPoints as unknown as BN & { divmod: (other: BN) => { div: BN; mod: BN }; }; const { div, mod } = basisPoints.divmod(power); const units = `${div.toString()}.${mod .abs() .toString(10, value.currency.decimals)}`; return `${value.currency.symbol} ${units}`; };