Precision and Types
The Velocity SDK uses integer big numbers (BN) for most values. Always convert using the SDK’s precision constants and helpers.
Precision Constants
Each constant is a BN representing a power of 10. Divide a raw value by its precision constant to get the human-readable number.
| Constant | Value | Used For |
|---|---|---|
PRICE_PRECISION | 1e6 | Oracle prices, order prices |
BASE_PRECISION | 1e9 | Perp position sizes |
QUOTE_PRECISION | 1e6 | USD amounts, collateral |
Converting Between Raw and Human-Readable Values
BN → Human Number
import { BN, PRICE_PRECISION, BASE_PRECISION, QUOTE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";
// Oracle price: raw 150_500_000 → 150.5 USD
const rawPrice = new BN(150_500_000);
const price = convertToNumber(rawPrice, PRICE_PRECISION);
console.log(price); // 150.5
// Position size: raw 2_500_000_000 → 2.5 SOL
const rawBase = new BN(2_500_000_000);
const size = convertToNumber(rawBase, BASE_PRECISION);
console.log(size); // 2.5
// Collateral: raw 10_000_000 → 10 USDT (or dUSDT on devnet)
const rawQuote = new BN(10_000_000);
const usd = convertToNumber(rawQuote, QUOTE_PRECISION);
console.log(usd); // 10Function convertToNumberReference ↗
Function convertToNumberReference ↗Converts a fixed-point `BN` into a human-readable JS `number` by dividing out `precision`. Correctly handles negative `bigNumber` values: `bn.js`'s `.div()`/`.mod()` truncate toward zero (matching JS `%` semantics), so the whole and fractional parts recombine with consistent sign without needing separate negative-number handling. Precision beyond what a JS `number` (IEEE-754 double) can represent exactly may be lost — this is a display/estimation helper, not for further on-chain-precision math.
| Parameter | Type | Required |
|---|---|---|
bigNumber | anyThe value to convert; `null`/`undefined` return 0 (a `BN` instance for
zero is still an object and thus truthy, so it falls through to the normal division below) | Yes |
precision | anyThe fixed-point precision to divide out; defaults to `PRICE_PRECISION` (1e6) | No |
| Returns |
|---|
number |
Human Number → BN
import { BN, BASE_PRECISION, PRICE_PRECISION } from "@velocity-exchange/sdk";
// 1 SOL in base precision
const oneSol = new BN(1).mul(BASE_PRECISION); // BN(1_000_000_000)
// $21.23 in price precision
const price = new BN(21_230_000); // 21.23 * 1e6
// Or use VelocityClient convenience helpers (available after setup):
const size = velocityClient.convertToPerpPrecision(1); // 1 base unit as BN
const px = velocityClient.convertToPricePrecision(21.23); // price as BN
const spot = velocityClient.convertToSpotPrecision(0, 100); // 100 quote-asset units as BNExample Human Number to BNReference ↗
Example Human Number to BNReference ↗Human Number to BN.BigNum: a value that carries its own precision
BigNum wraps a BN together with the number of decimal places that BN is scaled by, so a value and its precision travel as one object. Use it when you would otherwise hand-roll conversion and formatting: it prints, parses, and does arithmetic without you tracking the exponent by hand.
The second constructor argument is the exponent, not the precision constant. Pass PRICE_PRECISION_EXP (6), BASE_PRECISION_EXP (9), QUOTE_PRECISION_EXP (6), and so on, not PRICE_PRECISION (1e6).
import {
BN,
BigNum,
PRICE_PRECISION_EXP,
BASE_PRECISION_EXP,
} from "@velocity-exchange/sdk";
// Raw oracle price (1e6) wrapped with its exponent
const price = BigNum.from(new BN(150_500_000), PRICE_PRECISION_EXP);
price.print(); // "150.5"
price.toFixed(2); // "150.50"
price.toNotional(); // "$150.50"
price.toNum(); // 150.5
// Parse a user-entered string into the right precision
const size = BigNum.fromPrint("2.5", BASE_PRECISION_EXP);
size.toString(); // "2500000000" (the raw BN, 1e9)Class BigNumReference ↗
Class BigNumReference ↗| Property | Type | Required |
|---|---|---|
val | BN | Yes |
precision | BN | Yes |
bigNumFromParam | any | Yes |
add | (bn: BigNum) => BigNum | Yes |
sub | (bn: BigNum) => BigNum | Yes |
mul | (bn: any) => BigNum | Yes |
scalarMul | (bn: any) => BigNumMultiplies by another big number then scales the result down by the big number's precision so that we're in the same precision space | Yes |
div | (bn: any) => BigNum | Yes |
shift | (exponent: any, skipAdjustingPrecision?: boolean | undefined) => BigNumShift precision up or down | Yes |
shiftTo | (targetPrecision: BN) => BigNumShift to a target precision | Yes |
scale | (numerator: any, denominator: any) => BigNumScale the number by a fraction | Yes |
toPercentage | (denominator: BigNum, precision: number) => string | Yes |
gt | (bn: any, ignorePrecision?: boolean | undefined) => boolean | Yes |
lt | (bn: any, ignorePrecision?: boolean | undefined) => boolean | Yes |
gte | (bn: any, ignorePrecision?: boolean | undefined) => boolean | Yes |
lte | (bn: any, ignorePrecision?: boolean | undefined) => boolean | Yes |
eq | (bn: any, ignorePrecision?: boolean | undefined) => boolean | Yes |
eqZero | () => boolean | Yes |
gtZero | () => boolean | Yes |
ltZero | () => boolean | Yes |
gteZero | () => boolean | Yes |
lteZero | () => boolean | Yes |
abs | () => BigNum | Yes |
neg | () => BigNum | Yes |
toString | (base?: number | "hex" | undefined, length?: number | undefined) => string | Yes |
print | () => stringPretty print the underlying value in human-readable form. Depends on precision being correct for the output string to be correct | Yes |
prettyPrint | (useTradePrecision?: boolean | undefined, precisionOverride?: number | undefined, decimalOverride?: number | undefined) => string | Yes |
printShort | (useTradePrecision?: boolean | undefined, precisionOverride?: number | undefined) => stringPrint and remove unnecessary trailing zeroes | Yes |
debug | () => void | Yes |
toFixed | (fixedPrecision: number, rounded?: boolean | undefined) => stringPretty print with the specified number of decimal places | Yes |
getZeroes | any | Yes |
toRounded | (roundingPrecision: number) => BigNum | Yes |
toPrecision | (fixedPrecision: number, trailingZeroes?: boolean | undefined, rounded?: boolean | undefined) => stringPretty print to the specified number of significant figures | Yes |
toTradePrecision | (rounded?: boolean | undefined) => string | Yes |
toNotional | (useTradePrecision?: boolean | undefined, precisionOverride?: number | undefined, decimalOverride?: number | undefined) => stringPrint dollar formatted value. Defaults to fixed decimals two unless a given precision is given. | Yes |
toMillified | (precision?: number | undefined, rounded?: boolean | undefined, type?: "financial" | "scientific" | undefined) => string | Yes |
toJSON | () => { val: string; precision: string; } | Yes |
isNeg | () => boolean | Yes |
isPos | () => boolean | Yes |
toNum | () => numberGet the numerical value of the BigNum. This can break if the BigNum is too large. | Yes |
Useful methods, grouped by what they do:
| Group | Methods |
|---|---|
| Create | BigNum.from(val, exponent), BigNum.fromPrint(string, exponent), BigNum.zero(exponent), BigNum.fromJSON |
| Arithmetic | add, sub, mul, scalarMul, div, scale(numerator, denominator), abs, neg |
| Precision | shift(exponent), shiftTo(targetExponent) |
| Compare | gt, lt, gte, lte, eq, plus the zero checks gtZero, ltZero, eqZero, gteZero, lteZero |
print, printShort, prettyPrint, toFixed, toPrecision, toRounded, toNotional, toMillified, toPercentage | |
| Escape hatches | toString (raw BN string), toNum (JS number), toJSON |
Three behaviors worth knowing before you rely on it:
addandsubassert that both operands have the same exponent. CallshiftTofirst if they do not.mulreturns a value whose exponent is the sum of the two exponents.scalarMulshifts the result back down so it stays in the original precision space, which is usually what you want when multiplying a price by a ratio.toNumgoes throughparseFloat, so it loses accuracy on very large values. Keep money math inBigNumorBNand convert only for display.
BigNum.setLocale(locale) sets the decimal delimiter and thousands separator used by every printing method, globally for the class.
Slots Versus Wall Clock
Solana’s slot time is moving from 400ms down to 200ms through a feature-gate schedule (400, 350, 300, 250, 200). Because of that, a slot count is not a fixed amount of time. The protocol stores durations in milliseconds and converts them to slots using the live slot length, and math/time.ts gives you the same conversions off-chain.
The live slot length comes from the state account. Three fields drive it:
| State field | Meaning |
|---|---|
slotDurationMs | Current slot length in ms. 0 means unset and resolves to the 400ms baseline. |
pendingSlotDurationMs | A staged next value. 0 means nothing is staged. |
slotDurationEffectiveSlot | The slot at which the staged value takes over. |
Read it with activeSlotDurationFromState(state, currentSlot), which applies the staged switch once currentSlot reaches the effective slot. slotDurationFromState(raw) only resolves the 0 sentinel on the base field, so it keeps returning the pre-switch value across a gate flip.
import {
BN,
activeSlotDurationFromState,
millisFromSecs,
millisToSlotsCeil,
millisFromSlots,
msToSlotsCeilNum,
slotsToMsNum,
SLOT_DURATION_BASELINE,
} from "@velocity-exchange/sdk";
const state = velocityClient.getStateAccount();
const currentSlot = new BN(await connection.getSlot());
// The slot length the program itself would use right now
const slotDuration = activeSlotDurationFromState(state, currentSlot);
// A 10 second window, expressed in actual slots
const tenSeconds = millisFromSecs(10);
const slots = millisToSlotsCeil(tenSeconds, slotDuration); // 25 slots at 400ms, 50 at 200ms
// A measured slot delta, back to wall-clock ms
const elapsedMs = millisFromSlots(new BN(20), slotDuration);
// Plain-number variants for pacing and thresholds
const auctionSlots = msToSlotsCeilNum(8_000, slotDuration);
const auctionMs = slotsToMsNum(auctionSlots, slotDuration);
console.log(SLOT_DURATION_BASELINE); // 400, the pre-gate baselineFunction activeSlotDurationFromStateReference ↗
Function activeSlotDurationFromStateReference ↗activeSlotDurationFromState.The rounding direction matters and mirrors the program exactly:
| Helper | Rounding | Use for |
|---|---|---|
millisFromSlots(slots, d) | exact | Turning a measured slot delta into elapsed time |
millisToSlots(m, d) | down | Staleness windows, where shorter is the safe direction |
millisToSlotsCeil(m, d) | up | Windows that protect the user, such as auction lengths and minimum cooldowns |
msToSlotsNum(ms, d) | down | The same as millisToSlots, for plain numbers |
msToSlotsCeilNum(ms, d) | up | Durations that must not fall below their intended wall-clock length |
slotsToMsNum(slots, d) | exact | Plain-number version of millisFromSlots |
divPeriods(m, period) | down | How many whole periods fit in a duration, for legacy per-period rates |
Two more things to keep straight:
MillisandSlotDurationMsare branded types. A duration in milliseconds cannot be compared against a slot count without converting through the live slot length, and the type system enforces that. Build durations withmillis(ms)ormillisFromSecs(secs).STORED_UNIT_MS(400) is a storage codec for older admin-set fields that were encoded in units of the historical 400ms slot. Decode those withmillisFromStoredUnits. Do not use it as a general slots-to-seconds conversion factor.
Token Math Helpers
Spot balances are stored as interest-bearing scaled values. These helpers convert them to actual token amounts.
getTokenAmount converts a raw scaled spot balance into a token amount, accounting for accumulated interest since the last update. Pass the user’s scaledBalance, the spot market account (which contains the cumulative interest index), and the balance type (deposit or borrow).
import { getTokenAmount, SpotBalanceType, convertToNumber } from "@velocity-exchange/sdk";
const spotMarket = velocityClient.getSpotMarketAccount(0); // e.g. dUSDT on devnet, USDT on mainnet
const user = velocityClient.getUser();
const spotPosition = user.getUserAccount().spotPositions[0];
const tokenAmount = getTokenAmount(
spotPosition.scaledBalance,
spotMarket,
spotPosition.balanceType
);
console.log("Token amount:", tokenAmount.toString());Function getTokenAmountReference ↗
Function getTokenAmountReference ↗Calculates the spot token amount including any accumulated interest.
| Parameter | Type | Required |
|---|---|---|
balanceAmount | anyThe balance amount, typically from `SpotPosition.scaledBalance` | Yes |
spotMarket | SpotMarketAccountThe spot market account details | Yes |
balanceType | SpotBalanceTypeThe balance type to be used for calculation | Yes |
The calculated token amount, scaled by `SpotMarketConfig.precision`
| Returns |
|---|
BN |
getSignedTokenAmount wraps getTokenAmount to return a signed value: positive for deposits, negative for borrows. Use this when you need to distinguish between the two in a single number.
import { getTokenAmount, getSignedTokenAmount } from "@velocity-exchange/sdk";
const spotMarket = velocityClient.getSpotMarketAccount(0);
const spotPosition = velocityClient.getUser().getUserAccount().spotPositions[0];
const tokenAmount = getTokenAmount(
spotPosition.scaledBalance,
spotMarket,
spotPosition.balanceType
);
const signed = getSignedTokenAmount(tokenAmount, spotPosition.balanceType);
// signed > 0 means deposit, signed < 0 means borrow
console.log("Signed amount:", signed.toString());Function getSignedTokenAmountReference ↗
Function getSignedTokenAmountReference ↗Returns the signed (positive for deposit,negative for borrow) token amount based on the balance type.
| Parameter | Type | Required |
|---|---|---|
tokenAmount | anyThe token amount to convert (from `getTokenAmount`) | Yes |
balanceType | SpotBalanceTypeThe balance type to determine the sign of the token amount. | Yes |
- The signed token amount, scaled by `SpotMarketConfig.precision`
| Returns |
|---|
BN |