/** * @pwngh/economy-lab * * Copyright (c) Preston Neal * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import type { Rate } from './ports.js'; export { mulDiv } from './money.vendored.js'; export type { Rounding } from './money.vendored.js'; /** The currencies the system handles: in-app CREDIT and real-world USD. */ export type Currency = 'CREDIT' | 'USD'; /** * A money value: a currency plus an amount in minor units (cents for dollars). `minor` is * a `bigint` so it stays exact for the large totals that platform accounts reach, beyond * the point where `number` loses precision. * * `__brand` makes a plain `{ currency, minor }` unassignable to `Amount`. That forces * every amount through `toAmount` or `decodeAmount`, so the rules here cannot be bypassed. * * @see {@link https://economy-lab-docs.pages.dev/economy/concepts/money-model/ The money model} for * the exact-integer minor-unit design. */ export type Amount = { readonly currency: Currency; readonly minor: bigint; readonly __brand: 'Amount'; }; /** * Minor units per whole unit (100 cents = $1), exported so other code — fee rounding that * rounds up to a whole credit — shares this one factor. */ export declare const SCALE = 100n; /** * Builds an `Amount` from a currency and a minor-unit count. Throws AMOUNT_OVERFLOW when * the count falls outside the signed 64-bit range the ledger's BIGINT columns store, so * an unstorable amount fails at construction rather than at the database. * * @example * const price = toAmount('CREDIT', 1_500_000n); // 15,000 credits * const cash = toAmount('USD', 10_000n); // $100.00 */ export declare function toAmount(currency: Currency, minor: bigint): Amount; /** * Type guard for the `Amount` brand: true only for an object carrying the brand with a `bigint` * minor count — what `toAmount` and the decoders produce. Used where a value of unknown shape * (a decoded JSON tree, an operation field) must prove it is money before arithmetic touches it. */ export declare function isAmount(value: unknown): value is Amount; /** True at exactly zero minor units, in either currency. */ export declare function isZero(amount: Amount): boolean; /** True strictly below zero; zero itself is not negative. */ export declare function isNegative(amount: Amount): boolean; /** * Adds two amounts of the same currency. Throws CURRENCY_MISMATCH across currencies and * AMOUNT_OVERFLOW when the sum leaves the 64-bit range. */ export declare function add(a: Amount, b: Amount): Amount; /** * Flips an amount's sign in the same currency. Negating the minimum 64-bit value throws * AMOUNT_OVERFLOW, because its positive counterpart is one past the largest value the ledger * stores. */ export declare function negate(amount: Amount): Amount; /** * Orders two amounts of the same currency by minor units, so it drops straight into * `Array.sort`. Throws CURRENCY_MISMATCH across currencies. */ export declare function compare(a: Amount, b: Amount): -1 | 0 | 1; /** The zero amount of `currency` — the seed for summing balances or legs with {@link add}. */ export declare function zero(currency: Currency): Amount; /** * Builds a CREDIT `Amount` from a whole number of credits: `credits(120)` is 12,000 minor * units. A fractional count throws INVALID_AMOUNT; sub-credit amounts take `toAmount` with * minor units. Distinct from the ledger's `credit()`, which builds a posting leg. * * @example * credits(120); // equal to toAmount('CREDIT', 12_000n) * credits(20_000); // a payout-scale balance */ export declare function credits(whole: number | bigint): Amount; /** * Encodes an amount as text, such as `'CREDIT:12.34'`, for anywhere it leaves the program * (JSON, events, traces, HTTP). The result is a string because `JSON.stringify` cannot * serialize the `bigint`. It uses a fixed two decimals so the same amount always renders * identically; posting metadata uses this form so the bytes hashed into the tamper-evident * chain stay stable across replays. */ export declare function encodeAmount(amount: Amount): string; /** * Builds a USD amount from a decimal string, with the full strictness of `decodeAmount`: more * than two decimal places, digit grouping, or a value past the 64-bit range throws * INVALID_AMOUNT. * * @example * const price = usd('9.99'); // 999n minor units * const payoutValue = usd('100.00'); // what 20,000 credits cash out to at $0.005 per credit */ export declare function usd(decimal: string): Amount; /** * Parses a decimal string such as `'12.34'` or `'-0.05'` into an `Amount`. A bad format, * more than two decimal places, digit grouping (the canonical wire is ungrouped), or a * value past the 64-bit range throws INVALID_AMOUNT rather than silently accepting it. */ export declare function decodeAmount(decimal: string, currency: Currency): Amount; /** * Requires a positive CREDIT amount and returns it unchanged. A wrong currency or a non-positive * amount is a malformed request, not a recoverable decline, so it throws a fault. `label` names the * offending field in the error. */ export declare function requirePositiveCredit(amount: Amount, label: string): Amount; /** * Parses a wire amount such as `'CREDIT:12.34'` back into an `Amount`. The currency is the part * before the colon and must be one this system handles; anything else throws INVALID_AMOUNT, so a * string that merely contains a colon can never build an amount with a nonsense currency. This is * the inverse of `encodeAmount`, shared by every layer that stores or receives an amount as text: * the cache, the HTTP wire, and the SQL engines. */ export declare function decodeAmountWire(encoded: string): Amount; /** `a >= b` as rates, compared across scales without division. */ export declare function rateGte(a: Rate, b: Rate): boolean; /** * Converts an amount to another currency at `rate`, rounding down. A rate is an integer scaled by * `10^scale`, so the result is `floor(minor * rate / 10^scale)` — a true floor for either sign, * named as the mode at the division. Use it where rounding down is the safe direction, such as * paying a seller out: the sub-cent remainder stays with the platform instead of being minted. * Throws AMOUNT_OVERFLOW when the result leaves the 64-bit range; the multiply itself runs * through an unbounded intermediate, so it cannot overflow silently. * * @example * // cash out at a $0.005-per-credit CREDIT-to-USD rate: 5/10^3 USD minor per CREDIT minor * const rate = { rate: 5n, scale: 3, rateId: 'cashout-example' }; * convertFloor(toAmount('CREDIT', 2_000_100n), rate, 'USD'); * // 10_000n minor: the exact $100.005 floors to $100.00 */ export declare function convertFloor(amount: Amount, rate: Rate, to: Currency): Amount; /** * Converts an amount to another currency at `rate`, rounding up: `ceil(minor * rate / 10^scale)`, * a true ceiling for either sign. Use it where rounding down would under-cover, such as the USD a * top-up must hold in trust: the trust side rounds against the platform, never against the users * it backs. Throws AMOUNT_OVERFLOW when the result leaves the 64-bit range. * * @example * const rate = { rate: 5n, scale: 3, rateId: 'cashout-example' }; * convertCeil(toAmount('CREDIT', 2_000_100n), rate, 'USD'); * // 10_001n minor: the same $100.005 that convertFloor takes to $100.00 rounds up here */ export declare function convertCeil(amount: Amount, rate: Rate, to: Currency): Amount; /** * Walks any JSON-shaped value and swaps every branded {@link Amount} for its `CREDIT:12.34` wire * string. The one Amount-brand walk: the SQL engines use it to store an Operation in a JSON * column, and the HTTP store adapter uses it for the same Operation on the wire. A per-kind * branch would drift as the Operation union grows; the walk cannot. */ export declare function encodeAmounts(value: unknown): unknown; /** * Reverse of {@link encodeAmounts}: every string that parses as `CURRENCY:decimal` becomes an * Amount again; any other string (an idempotencyKey, a sku, a source, ...) passes through * unchanged. A string is an encoded amount only when the whole `decodeAmountWire` parse succeeds. */ export declare function decodeAmounts(value: unknown): unknown;