Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = any>(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<T = any>(text: string): T {
return parseJSON<T>(text);
}

/**
* Create a token client for a specific RWA token
*/
Expand Down
126 changes: 126 additions & 0 deletions sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":"<decimal>"}`, 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<string>,
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<T = any>(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<string, any> = {};
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<string, any> = {};
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<T = any>(text: string): T {
return JSON.parse(text, bigIntReviver) as T;
}