Skip to content

Commit

Permalink
feat: calcTotalPrice calcPrice calcUnits with bigint
Browse files Browse the repository at this point in the history
  • Loading branch information
itsmnthn committed Mar 21, 2024
1 parent ff2a408 commit c92477a
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 185 deletions.
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './web3'
export * from './units/unscale'
export * from './units/scale'
export * from './units/bigUtils'
export * from './units/price'

export const simulateAsyncPause = (duration = 1000) =>
new Promise<void>((resolve) => {
Expand Down
28 changes: 28 additions & 0 deletions packages/utils/src/units/price.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, test } from 'vitest'

import { calcPrice, calcTotalPrice, calcUnits } from './price'

test('calculate unit price from units and total price', () => {
expect(calcPrice(BigInt(2e6), BigInt(1e5), 6, 6)).toMatchInlineSnapshot('50000n')
expect(calcPrice(BigInt(100e18), BigInt(1e5), 18, 6)).toMatchInlineSnapshot('1000n')
expect(calcPrice(BigInt(-123e26), BigInt(1e5), 26, 6)).toMatchInlineSnapshot('813n')
expect(calcPrice(BigInt(-100e18), BigInt(1e5), 18, 6, true)).toMatchInlineSnapshot('-1000n')
})

test('calculate units from unit price and total price', () => {
expect(calcUnits(BigInt(1e5), BigInt(2e6))).toMatchInlineSnapshot('50000000000000000n')
expect(calcUnits('723400000', '2124000000', 6, 8)).toMatchInlineSnapshot('34058380n')
expect(calcUnits(BigInt(723400000e6), BigInt(21240000e6), 36, 18)).toMatchInlineSnapshot('34058380414312617702n')
expect(calcUnits('100000000', '1000000')).toMatchInlineSnapshot('100000000000000000000n')
expect(calcUnits('1000', '1200', 2, 2)).toMatchInlineSnapshot('83n')
})

test('calculate total price from units and unit price', () => {
expect(calcTotalPrice('100', '12', 2, 2)).toMatchInlineSnapshot('12n')
expect(calcTotalPrice(BigInt(2223e17), '12', 18, 1)).toMatchInlineSnapshot('2667n')
expect(calcTotalPrice(BigInt(2223e17), '1200000', 18, 6)).toMatchInlineSnapshot('266760000n')
expect(calcTotalPrice(BigInt(2223e17), '12000000', 18, 6)).toMatchInlineSnapshot('2667600000n')
expect(calcTotalPrice(BigInt(20e9), BigInt(2e6), 9, 6)).toMatchInlineSnapshot('40000000n')
expect(calcTotalPrice('20', '2', 0, 0)).toMatchInlineSnapshot('40n')
expect(calcTotalPrice('20', '2', 1, 0)).toMatchInlineSnapshot('4n')
})
64 changes: 64 additions & 0 deletions packages/utils/src/units/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { absBig } from './bigUtils'
import { scale } from './scale'
import { unScaleToBase } from './unscale'

const precisionCatalyst = 6 // to avoid loss of precision if the given decimals are not enough

/**
* Calculates the unit price based on the number of units and total price.
* @param noAbs pass `true` to allow -ve values for resulted unit price
* @example
* calcPrice(BigInt(2e6), BigInt(1e5), 6, 6) // 50000n
*/
export function calcPrice(
units: string | bigint, totalPrice: string | bigint, unitDecimals = 18, priceDecimals = 6, noAbs = false,
) {
if (!noAbs) // due to nature of the exchange, we need to support negative values for units and return +ve unit price
units = absBig(units)

// to avoid underflow and retain precision
const priceDecimalsFactor = priceDecimals + priceDecimals + unitDecimals + precisionCatalyst

return unScaleToBase(
scale(totalPrice.toString(), priceDecimalsFactor) / BigInt(units),
priceDecimalsFactor,
unitDecimals,
)
}

/**
* Calculates units based on the total price and number of units.
* @example
* calcUnits(BigInt(1e5), BigInt(2e6)) // 50000000000000000n
* calcUnits('1000', '1200', 2, 2) // 83n === 0.83 * 10^2
*/
export function calcUnits(
totalPrice: string | bigint, unitPrice: string | bigint, priceDecimals = 6, unitDecimals = 18,
) {
const priceDecimalsFactor = priceDecimals + priceDecimals + unitDecimals + precisionCatalyst
return unScaleToBase(
scale(totalPrice.toString(), priceDecimalsFactor) / BigInt(unitPrice),
priceDecimalsFactor,
unitDecimals,
)
}

/**
* Calculates the total price based on the number of units and unit price.
* @param noAbs pass `true` to allow -ve values for resulted total price
* @example
* calcTotalPrice('100', '12', 2, 2) // 12n
* BigInt(2223e17), '12000000', 18, 6) // 2667600000n
*/
export function calcTotalPrice(
units: string | bigint, unitPrice: string | bigint, unitDecimals = 18, priceDecimals = 6, noAbs = false,
) {
if (!noAbs) // due to nature of the exchange, we need to support negative values for units and return +ve total price
units = absBig(units)

return unScaleToBase(
BigInt(units) * BigInt(unitPrice),
unitDecimals + priceDecimals,
priceDecimals,
)
}
88 changes: 0 additions & 88 deletions packages/utils/src/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,94 +34,6 @@ export interface AmountFormat {

export type BigNumberish = BN | bigint | string | number

/**
* Scale the given number to the given decimals (e.g. 1 with 6 decimal -> 1000000)
* @param amount The number to scale
* @param decimals The number of decimals to scale to
* @returns The scaled number (e.g. 1 with 6 decimal -> 1000000)
*/
const scale = (amount: string | number | bigint, decimals: BigNumberish = 6): BN => {
return parseUnits(amount.toString(), decimals)
}

/**
* calculate total price from units using price per unit (e.g. units = 2e6, unitPrice = 1e5, unitDecimals = 6, priceDecimals = 6 => 0.2)
* @param units - The units to convert e.g. 2e6
* @param unitPrice - The unit price e.g. 1e5
* @param unitDecimals - The unit decimals e.g. 6
* @param priceDecimals - The price decimals e.g. 6
* @param doAbs - do Math.abs on value of units?
* @returns the total price for the given units using given unit price e.g. 0.2
*/
export const calcTotalPrice = (
units: BigNumberish,
unitPrice: BigNumberish,
unitDecimals = 6,
priceDecimals = 6,
doAbs = false,
): AmountFormat => {
if (doAbs)
units = BN.from(units).abs()
return getFormattedAmount(
scale(unitPrice.toString(), priceDecimals)
.mul(scale(units.toString(), priceDecimals))
.div(bnPow(priceDecimals + priceDecimals + unitDecimals))
.toString(),
priceDecimals,
)
}

/**
* calculate unit price from units and total price of units (e.g. units = 2e6, unitPrice = 1e5, unitDecimals = 6, priceDecimals = 6 => 0.2)
* @param units - The units to convert e.g. 2e6
* @param totalPrice - The unit price e.g. 1e5
* @param unitDecimals - The unit decimals e.g. 6
* @param priceDecimals - The price decimals e.g. 6
* @param doAbs - do Math.abs on value of units?
* @returns the total price for the given units using given unit price e.g. 0.2
*/
export const calcUnitPrice = (
units: BigNumberish,
totalPrice: BigNumberish,
unitDecimals = 6,
priceDecimals = 6,
doAbs = false,
): AmountFormat => {
if (doAbs)
units = BN.from(units).abs()
return getFormattedAmount(
BN.from(totalPrice)
.mul(bnPow(priceDecimals + unitDecimals))
.div(units)
.div(bnPow(priceDecimals))
.toString(),
priceDecimals,
)
}

/**
* calculate units from unit price and total price (e.g. unitPrice = 2e6, totalPrice = 1e5, priceDecimals = 6 => 0.05)
* @param unitPrice - The price of single unit e.g. 2e6
* @param totalPrice - The price of total units e.g. 1e5
* @param priceDecimals - The price decimals e.g. 6
* @returns the units for the given unit price and total price e.g. 0.05
*/
export const calcUnits = (
unitPrice: BigNumberish,
totalPrice: BigNumberish,
unitDecimals = 6,
priceDecimals = 6,
): AmountFormat => {
return getFormattedAmount(
BN.from(totalPrice)
.mul(bnPow(priceDecimals + unitDecimals))
.div(unitPrice)
.div(bnPow(priceDecimals))
.toString(),
unitDecimals,
)
}

/**
* returns a n percent of the given BigNumberish (e.g. 10% of 100 = 10)
* @param amount number to get percent of (e.g. 100)
Expand Down
97 changes: 0 additions & 97 deletions packages/utils/test/web3.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { formatUnits } from '@ethersproject/units'
import { describe, expect, it } from 'vitest'
import {
calcTotalPrice, calcUnitPrice, calcUnits,
decreaseNumByPercentage,
getFormattedAmount, getPercentOfAmount, getPercentageOfAmount, increaseNumByPercentage,
} from '../src'
Expand Down Expand Up @@ -82,102 +81,6 @@ describe('getPercentOfAmount', () => {
})
})

describe('calcTotalPrice', () => {
it('1. calculate total price for units using price per unit', () => {
const units = 214
const unitPrice = 201
const unitDecimals = 2
const priceDecimals = 2
const res = '430'
const out = calcTotalPrice(units, unitPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res)
})

it('2. calculate total price for units using price per unit', () => {
const units = 653748394
const unitPrice = 12032457
const unitDecimals = 6
const priceDecimals = 6
const res = getFormattedAmount('7866199439')
const out = calcTotalPrice(units, unitPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res.base)
})

it('3. calculate total price for units using price per unit', () => {
const units = '12000000000000000000'
const unitPrice = '1003306140'
const unitDecimals = 18
const priceDecimals = 6
const res = getFormattedAmount('12039673680')
const out = calcTotalPrice(units, unitPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res.base)
})
})

describe('calcUnitPrice', () => {
it('1. calculate 1 unit price for units and total price', () => {
const units = 214
const totalPrice = 430
const unitDecimals = 2
const priceDecimals = 2
const res = '200'
const out = calcUnitPrice(units, totalPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res)
})

it('2. calculate 1 unit price for units using total', () => {
const units = 653748394
const totalPrice = 7866199439
const unitDecimals = 6
const priceDecimals = 6
const res = getFormattedAmount('12032456')
const out = calcUnitPrice(units, totalPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res.base)
})

it('3. calculate 1 unit price for units using total', () => {
const units = '12000000000000000000'
const totalPrice = '12039673683'
const unitDecimals = 18
const priceDecimals = 6
const res = getFormattedAmount('1003306140')
const out = calcUnitPrice(units, totalPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res.base)
})
})

describe('calcUnits', () => {
it('1. calculate units from 1 unit price and total price', () => {
const unitPrice = 400
const totalPrice = 800
const priceDecimals = 2
const unitDecimals = 2
const res = '200'
const out = calcUnits(unitPrice, totalPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res)
})

it('2. calculate units from 1 unit price and total price', () => {
const unitPrice = 23566774
const totalPrice = '1273917398791872'
const unitDecimals = 6
const priceDecimals = 6
const res = getFormattedAmount('54055654744763')
const out = calcUnits(unitPrice, totalPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res.base)
})

it('3. calculate units from 1 unit price and total price', () => {
const unitPrice = 2e6
const totalPrice = 46e6
const unitDecimals = 18
const priceDecimals = 6
const res = getFormattedAmount('23000000000000000000')
const out = calcUnits(unitPrice, totalPrice, unitDecimals, priceDecimals)
expect(out.base).toEqual(res.base)
})
})

describe('getFormattedAmount', () => {
it('should format amount', () => {
const value = '1234567'
Expand Down

0 comments on commit c92477a

Please sign in to comment.