Created
May 7, 2025 20:22
-
-
Save asktree/f0a74a48fc6205f2cfae36460f7772ce to your computer and use it in GitHub Desktop.
fixed point BN type alias
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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