Skip to content

Instantly share code, notes, and snippets.

@asktree
Created May 7, 2025 20:22
Show Gist options
  • Save asktree/f0a74a48fc6205f2cfae36460f7772ce to your computer and use it in GitHub Desktop.
Save asktree/f0a74a48fc6205f2cfae36460f7772ce to your computer and use it in GitHub Desktop.
fixed point BN type alias
import BN from "bn.js"
import type { bignum } from "@metaplex-foundation/beet"
declare const scaleTag: unique symbol
type Build<N extends number, T extends unknown[] = []> = T["length"] extends N
? T
: Build<N, [...T, unknown]>
type Add<A extends number, B extends number> = [
...Build<A>,
...Build<B>,
]["length"] &
number
type Sub<A extends number, B extends number> =
Build<A> extends [...Build<B>, ...infer R] ? R["length"] : never
/// BN methods that are safe to use regardless of scale
type SafeBN = Pick<
BN,
"toNumber" | "toString" | "toJSON" | "toBuffer" | "toArray" | "toArrayLike"
>
/**
* The branded, compile-time-only fixed-point wrapper.
* At runtime it *is* a BN.
*
* Every mutating BN method keeps the same scale, therefore we
* just re-type its signature to use `Fixed<S>`.
*/
export type Fixed<S extends number> = SafeBN & {
readonly [scaleTag]: S
// ─── re-typed BN operators ──
add(other: Fixed<S>): Fixed<S>
sub(other: Fixed<S>): Fixed<S>
mul<B extends number>(other: Fixed<B>): Fixed<Add<S, B>>
div<B extends number>(other: Fixed<B>): Fixed<Sub<S, B>>
mod(other: Fixed<S>): Fixed<S>
} //& { __brand?: never } // keeps it "opaque"
/* ───── overloads ───────────────────────────────────────────── */
type BNInput = ConstructorParameters<typeof BN>[0]
/** Wraps a BN or BNLike to a Fixed<S>. Tiny runtime cost (one BN.isBN check). */
export function fixed<S extends number>(x: BN): Fixed<S>
export function fixed<S extends number>(x: BNInput): Fixed<S>
export function fixed<S extends number>(x: BN | BNInput): Fixed<S> {
return BN.isBN(x)
? (x as unknown as Fixed<S>)
: (new BN(x) as unknown as Fixed<S>)
}
export function unwrap<S extends number>(x: Fixed<S>): BN {
return x as unknown as BN
}
// -----------------------------------------------------------------------------
// pre‑computed 10ⁿ table is a little faster than 10n ** BigInt(n) every time
// -----------------------------------------------------------------------------
const POW10 = [
new BN(1),
new BN(10),
new BN(100),
new BN(1_000),
new BN(10_000),
new BN(100_000),
new BN(1_000_000),
new BN(10_000_000),
new BN(100_000_000),
new BN(1_000_000_000),
new BN(10_000_000_000),
new BN(100_000_000_000),
new BN(1_000_000_000_000),
new BN(10_000_000_000_000),
new BN(100_000_000_000_000),
new BN("1_000_000_000_000_000"),
new BN("10_000_000_000_000_000"),
new BN("100_000_000_000_000_000"),
new BN("1_000_000_000_000_000_000"), // 18 (ETH‑style)
new BN("10_000_000_000_000_000_000"), // 19 (room to grow)
] as const
export type Rounding = "floor" | "ceil" | "round"
/**
* Convert `Fixed<From>` to `Fixed<To>` by shifting powers of ten.
*
* @param x bigint in the original scale
* @param fromScale compile‑time & runtime scale of `x`
* @param toScale target scale
* @param mode how to handle lost precision when scaling down (default: floor)
*/
export function rescale<From extends number, To extends number>(
x: Fixed<From>,
fromScale: From,
toScale: To,
mode: Rounding = "floor"
): Fixed<To> {
if (fromScale === (toScale as number)) return x as unknown as Fixed<To>
const diff = toScale - fromScale
const factor = POW10[Math.abs(diff) as keyof typeof POW10] as BN
if (diff > 0) {
// upscale: append zeros
return unwrap(x).mul(factor) as unknown as Fixed<To>
}
// downscale: drop digits
const quotient = unwrap(x).div(factor)
if (mode === "floor") return quotient as unknown as Fixed<To>
const remainder = unwrap(x).mod(factor)
if (remainder.eqn(0)) return quotient as unknown as Fixed<To>
if (mode === "ceil") {
return quotient.addn(1) as unknown as Fixed<To>
}
// "round": half‑up
const half = factor.divn(2)
return quotient.addn(remainder.gte(half) ? 1 : 0) as unknown as Fixed<To>
}
const test = () => {
const a = fixed<9>(new BN(1000))
const b = fixed<6>(new BN(2000))
const c = a.mul(b)
const d: bignum = c
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment