diff --git a/sdk/src/index.ts b/sdk/src/index.ts index b1ea0de..7adfcc3 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -383,6 +383,31 @@ export class StellarRWASDK { this.logger.info('SDK initialized successfully'); } + /** + * Serialize any value (SDK result, portfolio, order book, …) to a + * JSON-safe structure with every BigInt rendered as a decimal string. + * Use this when returning SDK data from a JSON HTTP API. (Issue #193) + */ + toJSONSafe(value: T): any { + return serializeBigInts(value); + } + + /** + * `JSON.stringify` with lossless BigInt support (tagged `{"$bigint":"n"}` + * format restored by {@link StellarRWASDK.parseJSON}). (Issue #193) + */ + stringifyJSON(value: any, space?: string | number): string { + return stringifyJSON(value, space); + } + + /** + * `JSON.parse` that restores BigInt values produced by + * {@link StellarRWASDK.stringifyJSON}. (Issue #193) + */ + parseJSON(text: string): T { + return parseJSON(text); + } + /** * Create a token client for a specific RWA token */ diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 42ceb0d..adea21a 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -682,3 +682,129 @@ export interface WebhookConfig { events: string[]; active: boolean; } + +// --------------------------------------------------------------------------- +// BigInt JSON serialization (Issue #193) +// --------------------------------------------------------------------------- +// +// `JSON.stringify` throws on `bigint` values. These helpers make BigInt safe to +// send over a JSON API and to parse back losslessly. +// +// Two modes: +// 1. Plain-string mode (`bigIntReplacer`) — every BigInt becomes a decimal +// string. Use for external API responses where the consumer treats large +// integers as strings. Round-tripping requires knowing which fields are +// BigInt (see `createBigIntReviver`). +// 2. Tagged mode (`stringifyJSON` / `parseJSON`) — every BigInt becomes +// `{"$bigint":""}`, which `parseJSON` restores to a real BigInt. +// Use for internal persistence / caches that must round-trip exactly. + +/** Marker key used by the tagged serialization format. */ +export const BIGINT_TAG = '$bigint' as const; + +export interface TaggedBigInt { + [BIGINT_TAG]: string; +} + +export function isTaggedBigInt(value: unknown): value is TaggedBigInt { + return ( + typeof value === 'object' && + value !== null && + typeof (value as any)[BIGINT_TAG] === 'string' && + Object.keys(value as any).length === 1 + ); +} + +/** + * `JSON.stringify` replacer that renders every `bigint` as a decimal string. + * Also normalises `BigInt`-like objects (e.g. `bignumber.js`) via `toString()` + * when they expose an `isBigNumber`/`_isBigNumber` flag. + */ +export function bigIntReplacer(_key: string, value: any): any { + if (typeof value === 'bigint') return value.toString(); + if ( + value && + typeof value === 'object' && + (value._isBigNumber === true || value.isBigNumber === true) && + typeof value.toString === 'function' + ) { + return value.toString(); + } + return value; +} + +/** + * `JSON.stringify` replacer for the tagged format: `bigint` -> `{$bigint: "n"}`. + */ +export function taggedBigIntReplacer(_key: string, value: any): any { + return typeof value === 'bigint' ? { [BIGINT_TAG]: value.toString() } : value; +} + +/** + * `JSON.parse` reviver for the tagged format: `{$bigint: "n"}` -> `bigint`. + */ +export function bigIntReviver(_key: string, value: any): any { + return isTaggedBigInt(value) ? BigInt(value[BIGINT_TAG]) : value; +} + +const BIGINT_STRING_RE = /^-?\d+$/; + +/** + * Build a `JSON.parse` reviver that converts the string value of the named keys + * back into `bigint`. Use with output produced by {@link bigIntReplacer} when + * the BigInt-bearing field names are known. + * + * @param keys Field names whose string values should become BigInt. + * @param options.onlyUnsafe Only convert when the number exceeds + * `Number.MAX_SAFE_INTEGER` (default: false — always convert). + */ +export function createBigIntReviver( + keys: Iterable, + options: { onlyUnsafe?: boolean } = {} +): (key: string, value: any) => any { + const keySet = new Set(keys); + return (key: string, value: any) => { + if (isTaggedBigInt(value)) return BigInt(value[BIGINT_TAG]); + if (typeof value === 'string' && keySet.has(key) && BIGINT_STRING_RE.test(value)) { + if (options.onlyUnsafe && Number.isSafeInteger(Number(value))) return value; + return BigInt(value); + } + return value; + }; +} + +/** + * Recursively convert every `bigint` in a value to a decimal string. Handles + * nested objects, arrays, `Map` and `Set`. Non-mutating. + */ +export function serializeBigInts(input: T): any { + if (typeof input === 'bigint') return input.toString(); + if (Array.isArray(input)) return input.map((v) => serializeBigInts(v)); + if (input instanceof Map) { + const out: Record = {}; + for (const [k, v] of input) out[String(k)] = serializeBigInts(v); + return out; + } + if (input instanceof Set) return [...input].map((v) => serializeBigInts(v)); + if (input && typeof input === 'object') { + if (input instanceof Date) return input; + const out: Record = {}; + for (const [k, v] of Object.entries(input)) out[k] = serializeBigInts(v); + return out; + } + return input; +} + +/** + * `JSON.stringify` with lossless BigInt support (tagged format). + */ +export function stringifyJSON(value: any, space?: string | number): string { + return JSON.stringify(value, taggedBigIntReplacer, space); +} + +/** + * `JSON.parse` that restores BigInt values written by {@link stringifyJSON}. + */ +export function parseJSON(text: string): T { + return JSON.parse(text, bigIntReviver) as T; +}