From be92d3d0821a0d2f61b511679a5772383ead3a17 Mon Sep 17 00:00:00 2001 From: "Rai (AI agent)" Date: Thu, 24 Sep 2026 00:34:22 +0000 Subject: [PATCH] feat(sdk): add Nano (XNO) amount primitives (XnoAmount, xno, formatXno) Adds a self-contained XNO amount type alongside the existing USDC primitives in packages/sdk/src/types.ts: - XnoAmount - 30-decimal bigint interface (mirrors UsdcAmount) - XNO_DECIMALS = 30 - Nano native raw precision - xno(human) - decimal-string parser, exact down to a single raw - formatXno(amount) - renders back to a trimmed human string - NANO_NETWORK = "nano:mainnet" - CAIP-2 chain id for x402 accepts Already exported via export * from "./types.js" in index.ts. No new dependencies. All 9 monorepo packages build and typecheck clean. --- packages/sdk/src/types.ts | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 52dd659..0652a6d 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -21,3 +21,61 @@ export function formatUsdc(amount: UsdcAmount): string { const fractional = amount.baseUnits % 1_000_000n; return `${whole}.${fractional.toString().padStart(6, "0")}`; } + +export interface XnoAmount { + /** + * Amount in Nano raw (1 XNO = 10^30 raw). A 30-decimal unsigned integer, + * represented as a bigint so a sub-cent price keeps its exact value when + * summed across a billing run. + */ + raw: bigint; +} + +export const XNO_DECIMALS = 30; + +/** + * Nano raw units per whole XNO (10^30). Distinct from the 6-decimal base unit + * of USDC: a $0.0005 per-call charge is exactly representable at 30 decimals + * but rounds to zero base units in `UsdcAmount`. + */ +export const XNO_RAW_PER_XNO = 10n ** 30n; + +/** + * CAIP-2 chain id for the Nano mainnet ledger, as it appears in an x402 + * `accepts[]` entry (`network: "nano:mainnet", asset: "XNO"`). + */ +export const NANO_NETWORK = "nano:mainnet" as const; + +/** + * Build an `XnoAmount` from a human XNO string, exact down to a single raw unit. + * + * Decimal-string parsing (not float arithmetic) keeps `xno("0.0005").raw` + * exactly `5e26` — a value float math would corrupt because 10^30 exceeds + * `Number.MAX_SAFE_INTEGER`. + */ +export function xno(human: number | string): XnoAmount { + const text = typeof human === "number" ? String(human) : human.trim(); + if (text === "" || !/^-?\d*\.?\d*$/.test(text)) { + throw new Error(`Invalid XNO amount: ${human}`); + } + if (text.startsWith("-")) { + throw new Error(`XNO amounts must be non-negative: ${human}`); + } + const [whole = "0", frac = ""] = text.split("."); + if (frac.length > XNO_DECIMALS) { + throw new Error( + `XNO amount has more than ${XNO_DECIMALS} decimal places: ${human}` + ); + } + const raw = BigInt(whole) * XNO_RAW_PER_XNO + BigInt(frac.padEnd(XNO_DECIMALS, "0") || "0"); + return { raw }; +} + +/** Render an `XnoAmount` back to a trimmed human string. */ +export function formatXno(amount: XnoAmount): string { + const whole = amount.raw / XNO_RAW_PER_XNO; + const frac = amount.raw % XNO_RAW_PER_XNO; + if (frac === 0n) return whole.toString(); + const fracStr = frac.toString().padStart(XNO_DECIMALS, "0").replace(/0+$/, ""); + return `${whole}.${fracStr}`; +}