@forgezero/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/contracts/foundry.toml +9 -0
  4. package/contracts/src/ColdVault.sol +206 -0
  5. package/contracts/src/DepositFactory.sol +202 -0
  6. package/contracts/src/DepositProxy.sol +72 -0
  7. package/contracts/src/IERC20.sol +7 -0
  8. package/contracts/src/MockTokens.sol +32 -0
  9. package/contracts/src/SafeTransferLib.sol +31 -0
  10. package/contracts/test/Custody.t.sol +361 -0
  11. package/contracts/test/Vectors.t.sol +45 -0
  12. package/dist/audit.d.ts +265 -0
  13. package/dist/audit.js +291 -0
  14. package/dist/backup.d.ts +243 -0
  15. package/dist/backup.js +302 -0
  16. package/dist/calendar.d.ts +136 -0
  17. package/dist/calendar.js +129 -0
  18. package/dist/compliance.d.ts +172 -0
  19. package/dist/compliance.js +168 -0
  20. package/dist/finance/binance.d.ts +27 -0
  21. package/dist/finance/binance.js +452 -0
  22. package/dist/finance/chain-addresses.d.ts +130 -0
  23. package/dist/finance/chain-addresses.js +462 -0
  24. package/dist/finance/chain-deposits.d.ts +193 -0
  25. package/dist/finance/chain-deposits.js +596 -0
  26. package/dist/finance/chain-reconcile.d.ts +112 -0
  27. package/dist/finance/chain-reconcile.js +76 -0
  28. package/dist/finance/chain-withdrawals.d.ts +223 -0
  29. package/dist/finance/chain-withdrawals.js +631 -0
  30. package/dist/finance/chain.d.ts +116 -0
  31. package/dist/finance/chain.js +316 -0
  32. package/dist/finance/commission.d.ts +155 -0
  33. package/dist/finance/commission.js +419 -0
  34. package/dist/finance/custody.d.ts +68 -0
  35. package/dist/finance/custody.js +107 -0
  36. package/dist/finance/derive.d.ts +115 -0
  37. package/dist/finance/derive.js +116 -0
  38. package/dist/finance/discounts.d.ts +98 -0
  39. package/dist/finance/discounts.js +90 -0
  40. package/dist/finance/ledger.d.ts +221 -0
  41. package/dist/finance/ledger.js +308 -0
  42. package/dist/finance/market.d.ts +209 -0
  43. package/dist/finance/market.js +112 -0
  44. package/dist/finance/money.d.ts +118 -0
  45. package/dist/finance/money.js +176 -0
  46. package/dist/finance/rates.d.ts +178 -0
  47. package/dist/finance/rates.js +292 -0
  48. package/dist/finance/storage.d.ts +113 -0
  49. package/dist/finance/storage.js +226 -0
  50. package/dist/finance/tax.d.ts +132 -0
  51. package/dist/finance/tax.js +291 -0
  52. package/dist/finance/transfers.d.ts +153 -0
  53. package/dist/finance/transfers.js +292 -0
  54. package/dist/finance/venues.d.ts +190 -0
  55. package/dist/finance/venues.js +251 -0
  56. package/dist/identity.d.ts +115 -0
  57. package/dist/identity.js +111 -0
  58. package/dist/importers.d.ts +87 -0
  59. package/dist/importers.js +250 -0
  60. package/dist/jobs.d.ts +171 -0
  61. package/dist/jobs.js +250 -0
  62. package/dist/notify-templates.d.ts +11 -0
  63. package/dist/notify-templates.js +254 -0
  64. package/dist/notify.d.ts +172 -0
  65. package/dist/notify.js +122 -0
  66. package/dist/openssh.d.ts +36 -0
  67. package/dist/openssh.js +106 -0
  68. package/dist/otpauth.d.ts +57 -0
  69. package/dist/otpauth.js +223 -0
  70. package/dist/outbox.d.ts +234 -0
  71. package/dist/outbox.js +236 -0
  72. package/dist/passkey.d.ts +120 -0
  73. package/dist/passkey.js +105 -0
  74. package/dist/phrase.d.ts +87 -0
  75. package/dist/phrase.js +87 -0
  76. package/dist/pipeline.d.ts +137 -0
  77. package/dist/pipeline.js +121 -0
  78. package/dist/queue.d.ts +243 -0
  79. package/dist/queue.js +246 -0
  80. package/dist/schema-typebox.d.ts +24 -0
  81. package/dist/schema-typebox.js +201 -0
  82. package/dist/schema.d.ts +134 -0
  83. package/dist/schema.js +169 -0
  84. package/dist/serial.d.ts +54 -0
  85. package/dist/serial.js +40 -0
  86. package/dist/slip10.d.ts +37 -0
  87. package/dist/slip10.js +74 -0
  88. package/dist/snp.d.ts +115 -0
  89. package/dist/snp.js +109 -0
  90. package/dist/ssh-agent.d.ts +70 -0
  91. package/dist/ssh-agent.js +141 -0
  92. package/dist/ssh-cert.d.ts +73 -0
  93. package/dist/ssh-cert.js +111 -0
  94. package/dist/totp.d.ts +104 -0
  95. package/dist/totp.js +143 -0
  96. package/package.json +248 -0
@@ -0,0 +1,292 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/finance/money.ts
10
+ class MoneyError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = "MoneyError";
16
+ }
17
+ }
18
+ var ASSETS = [
19
+ { code: "USDT", decimals: 6 },
20
+ { code: "USDC", decimals: 6 },
21
+ { code: "BTC", decimals: 8 },
22
+ { code: "ETH", decimals: 18 },
23
+ { code: "BNB", decimals: 18 },
24
+ { code: "EUR", decimals: 2 },
25
+ { code: "USD", decimals: 2 }
26
+ ];
27
+ var REGISTRY = new Map(ASSETS.map((asset) => [asset.code, asset]));
28
+ function defineAsset(spec) {
29
+ if (spec.decimals < 0 || spec.decimals > 30 || !Number.isInteger(spec.decimals)) {
30
+ throw new MoneyError("UNKNOWN_ASSET", `${spec.code}: decimals must be an integer 0–30.`);
31
+ }
32
+ REGISTRY.set(spec.code, spec);
33
+ }
34
+ function assetSpec(code) {
35
+ const spec = REGISTRY.get(code);
36
+ if (!spec)
37
+ throw new MoneyError("UNKNOWN_ASSET", `Unknown asset "${code}". Call defineAsset first.`);
38
+ return spec;
39
+ }
40
+ var money = (units, asset) => {
41
+ assetSpec(asset);
42
+ return { units, asset };
43
+ };
44
+ var zero = (asset) => money(0n, asset);
45
+ function parseAmount(value, asset) {
46
+ const spec = assetSpec(asset);
47
+ const text = value.trim();
48
+ if (!/^-?\d+(\.\d+)?$/.test(text)) {
49
+ throw new MoneyError("NOT_FINITE", `"${value}" is not a plain decimal amount.`);
50
+ }
51
+ const negative = text.startsWith("-");
52
+ const [whole, fraction = ""] = text.replace("-", "").split(".");
53
+ if (fraction.length > spec.decimals) {
54
+ throw new MoneyError("PRECISION_LOSS", `${asset} has ${spec.decimals} decimals; "${value}" has ${fraction.length}.`);
55
+ }
56
+ const padded = fraction.padEnd(spec.decimals, "0");
57
+ const units = BigInt(whole + padded);
58
+ return { units: negative ? -units : units, asset };
59
+ }
60
+ function formatAmount(amount, options = {}) {
61
+ const spec = assetSpec(amount.asset);
62
+ const negative = amount.units < 0n;
63
+ const digits = (negative ? -amount.units : amount.units).toString().padStart(spec.decimals + 1, "0");
64
+ const whole = digits.slice(0, digits.length - spec.decimals);
65
+ let fraction = spec.decimals === 0 ? "" : digits.slice(digits.length - spec.decimals);
66
+ if (options.trim && fraction)
67
+ fraction = fraction.replace(/0+$/, "");
68
+ return `${negative ? "-" : ""}${whole}${fraction ? `.${fraction}` : ""}`;
69
+ }
70
+ function sameAsset(a, b) {
71
+ if (a.asset !== b.asset) {
72
+ throw new MoneyError("ASSET_MISMATCH", `Cannot combine ${a.asset} and ${b.asset}.`);
73
+ }
74
+ }
75
+ function add(a, b) {
76
+ sameAsset(a, b);
77
+ return { units: a.units + b.units, asset: a.asset };
78
+ }
79
+ function subtract(a, b) {
80
+ sameAsset(a, b);
81
+ return { units: a.units - b.units, asset: a.asset };
82
+ }
83
+ var negate = (amount) => ({ units: -amount.units, asset: amount.asset });
84
+ var abs = (amount) => ({
85
+ units: amount.units < 0n ? -amount.units : amount.units,
86
+ asset: amount.asset
87
+ });
88
+ var isZero = (amount) => amount.units === 0n;
89
+ var isNegative = (amount) => amount.units < 0n;
90
+ function compare(a, b) {
91
+ sameAsset(a, b);
92
+ return a.units < b.units ? -1 : a.units > b.units ? 1 : 0;
93
+ }
94
+ var equals = (a, b) => a.asset === b.asset && a.units === b.units;
95
+ var ROUNDING = ["down", "up", "half-up"];
96
+ function divideRounded(numerator, denominator, mode) {
97
+ if (denominator === 0n)
98
+ throw new MoneyError("DIVIDE_BY_ZERO", "Division by zero.");
99
+ const negative = numerator < 0n !== denominator < 0n;
100
+ const a = numerator < 0n ? -numerator : numerator;
101
+ const b = denominator < 0n ? -denominator : denominator;
102
+ const quotient = a / b;
103
+ const remainder = a % b;
104
+ if (remainder === 0n)
105
+ return negative ? -quotient : quotient;
106
+ let result = quotient;
107
+ if (mode === "up")
108
+ result += 1n;
109
+ else if (mode === "half-up" && remainder * 2n >= b)
110
+ result += 1n;
111
+ return negative ? -result : result;
112
+ }
113
+ function mulRate(amount, rate, mode = "down") {
114
+ if (!/^-?\d+(\.\d+)?$/.test(rate.trim())) {
115
+ throw new MoneyError("NOT_FINITE", `"${rate}" is not a plain decimal rate.`);
116
+ }
117
+ const [whole, fraction = ""] = rate.trim().replace("-", "").split(".");
118
+ const scale = 10n ** BigInt(fraction.length);
119
+ const scaled = BigInt(whole + fraction) * (rate.trim().startsWith("-") ? -1n : 1n);
120
+ return { units: divideRounded(amount.units * scaled, scale, mode), asset: amount.asset };
121
+ }
122
+ function convert(amount, to, rate, mode = "down") {
123
+ const from = assetSpec(amount.asset);
124
+ const target = assetSpec(to);
125
+ const asTarget = mulRate({ units: amount.units, asset: to }, rate, mode);
126
+ const shift = target.decimals - from.decimals;
127
+ if (shift === 0)
128
+ return asTarget;
129
+ if (shift > 0)
130
+ return { units: asTarget.units * 10n ** BigInt(shift), asset: to };
131
+ return { units: divideRounded(asTarget.units, 10n ** BigInt(-shift), mode), asset: to };
132
+ }
133
+ function allocate(amount, parts) {
134
+ if (parts < 1)
135
+ throw new MoneyError("NOT_FINITE", "Cannot allocate into fewer than one part.");
136
+ const each = divideRounded(amount.units, BigInt(parts), "down");
137
+ const allocated = Array.from({ length: parts }, () => each);
138
+ let remainder = amount.units - each * BigInt(parts);
139
+ const step = remainder < 0n ? -1n : 1n;
140
+ for (let index = 0;remainder !== 0n; index = (index + 1) % parts) {
141
+ allocated[index] += step;
142
+ remainder -= step;
143
+ }
144
+ return allocated.map((units) => ({ units, asset: amount.asset }));
145
+ }
146
+ function toStep(amount, step, mode = "down") {
147
+ const stepUnits = parseAmount(step, amount.asset).units;
148
+ if (stepUnits <= 0n)
149
+ throw new MoneyError("NOT_FINITE", "A step must be positive.");
150
+ return { units: divideRounded(amount.units, stepUnits, mode) * stepUnits, asset: amount.asset };
151
+ }
152
+ var VERSION = "0.1.0";
153
+
154
+ // src/finance/rates.ts
155
+ class RateError extends Error {
156
+ code;
157
+ asset;
158
+ constructor(code, message, asset) {
159
+ super(message);
160
+ this.code = code;
161
+ this.asset = asset;
162
+ this.name = "RateError";
163
+ }
164
+ }
165
+ var BASE_ASSET = "USD";
166
+ var RATE_SOURCES = ["peg", "venue", "manual"];
167
+ var DECIMAL = /^\d+(\.\d+)?$/;
168
+ function assertRate(rate) {
169
+ if (!DECIMAL.test(rate.usd.trim())) {
170
+ throw new RateError("BAD_RATE", `"${rate.usd}" is not a plain positive decimal.`, rate.asset);
171
+ }
172
+ if (Number(rate.usd) <= 0) {
173
+ throw new RateError("BAD_RATE", `A rate of ${rate.usd} for ${rate.asset} cannot be right.`, rate.asset);
174
+ }
175
+ if (rate.source === "peg" && rate.atMs !== undefined) {
176
+ throw new RateError("BAD_RATE", `${rate.asset} is pegged, so it has no timestamp — a peg that appears to age invites somebody to refresh it.`, rate.asset);
177
+ }
178
+ if (rate.source !== "peg" && rate.atMs === undefined) {
179
+ throw new RateError("BAD_RATE", `${rate.asset} is quoted from ${rate.source}, so it must carry when it was true. A rate with no age keeps quoting yesterday after the feed dies.`, rate.asset);
180
+ }
181
+ }
182
+ var peg = (asset, usd = "1") => ({ asset, source: "peg", usd });
183
+ function createRateTable(options = {}) {
184
+ const maxAgeMs = options.maxAgeMs ?? 15 * 60000;
185
+ const now = options.now ?? Date.now;
186
+ const rates = new Map;
187
+ return {
188
+ set(rate) {
189
+ assertRate(rate);
190
+ rates.set(rate.asset, rate);
191
+ },
192
+ peek: (asset) => rates.get(asset),
193
+ ageOf(asset) {
194
+ const rate = rates.get(asset);
195
+ return rate?.atMs === undefined ? undefined : now() - rate.atMs;
196
+ },
197
+ isStale(asset) {
198
+ const rate = rates.get(asset);
199
+ if (!rate)
200
+ return true;
201
+ if (rate.source === "peg")
202
+ return false;
203
+ return now() - (rate.atMs ?? 0) > maxAgeMs;
204
+ },
205
+ require(asset) {
206
+ const rate = rates.get(asset);
207
+ if (!rate) {
208
+ throw new RateError("NO_RATE", `No USD rate is configured for ${asset}. Add a peg, a venue source, or a manual value.`, asset);
209
+ }
210
+ if (rate.source !== "peg" && now() - (rate.atMs ?? 0) > maxAgeMs) {
211
+ throw new RateError("STALE_RATE", `The ${asset} rate is ${Math.round((now() - (rate.atMs ?? 0)) / 60000)} minutes old; the limit is ${Math.round(maxAgeMs / 60000)}.`, asset);
212
+ }
213
+ return rate;
214
+ },
215
+ toUsd(amount) {
216
+ if (amount.asset === BASE_ASSET)
217
+ return amount;
218
+ return convert(amount, BASE_ASSET, this.require(amount.asset).usd, "down");
219
+ },
220
+ totalUsd(amounts) {
221
+ let total = parseAmount("0", BASE_ASSET);
222
+ for (const amount of amounts) {
223
+ const valued = this.toUsd(amount);
224
+ total = { units: total.units + valued.units, asset: BASE_ASSET };
225
+ }
226
+ return total;
227
+ },
228
+ all: () => [...rates.values()],
229
+ clear: () => rates.clear()
230
+ };
231
+ }
232
+ function registerAsset(entry) {
233
+ defineAsset({ code: entry.code, decimals: entry.decimals, label: entry.label });
234
+ assertRate(entry.rate);
235
+ }
236
+ var isRegistered = (code) => {
237
+ try {
238
+ assetSpec(code);
239
+ return true;
240
+ } catch {
241
+ return false;
242
+ }
243
+ };
244
+ async function refreshRates(table, fetcher, args) {
245
+ const now = args.now ?? Date.now;
246
+ const report = { refreshed: [], failed: [], skipped: [] };
247
+ for (const entry of args.assets) {
248
+ if (!entry.enabled || entry.rate.source !== "venue") {
249
+ report.skipped.push(entry.code);
250
+ continue;
251
+ }
252
+ try {
253
+ const usd = await fetcher.quote(entry.code);
254
+ if (!usd) {
255
+ report.failed.push({ asset: entry.code, reason: "the source does not quote it" });
256
+ continue;
257
+ }
258
+ table.set({ asset: entry.code, source: "venue", usd, atMs: now(), via: entry.rate.via });
259
+ report.refreshed.push(entry.code);
260
+ } catch (cause) {
261
+ report.failed.push({
262
+ asset: entry.code,
263
+ reason: cause instanceof Error ? cause.message : "unknown error"
264
+ });
265
+ }
266
+ }
267
+ return report;
268
+ }
269
+ function rateRefreshJob(args) {
270
+ return {
271
+ key: args.key ?? "rates.refresh",
272
+ everyMs: args.everyMs ?? 5 * 60000,
273
+ run: async () => {
274
+ const report = await refreshRates(args.table, args.fetcher, { assets: args.assets() });
275
+ return { ok: report.failed.length === 0, detail: report };
276
+ }
277
+ };
278
+ }
279
+ var describeRate = (rate, ageMs) => rate.source === "peg" ? `1 ${rate.asset} = ${formatAmount(parseAmount(rate.usd, BASE_ASSET), { trim: true })} USD, pegged` : `1 ${rate.asset} = ${rate.usd} USD from ${rate.via ?? rate.source}, ${ageMs === undefined ? "age unknown" : `${Math.round(ageMs / 1000)}s old`}`;
280
+ export {
281
+ registerAsset,
282
+ refreshRates,
283
+ rateRefreshJob,
284
+ peg,
285
+ isRegistered,
286
+ describeRate,
287
+ createRateTable,
288
+ assertRate,
289
+ RateError,
290
+ RATE_SOURCES,
291
+ BASE_ASSET
292
+ };
@@ -0,0 +1,113 @@
1
+ import { type Money } from './money';
2
+ /**
3
+ * How an amount is written to a database, and read back exactly.
4
+ *
5
+ * A ledger needs two incompatible things from the same value: perfect accuracy,
6
+ * and the ability to `SORT` and range-filter it. No single column gives both.
7
+ *
8
+ * ACCURACY `0.1` is not representable in IEEE-754, and 18-decimal minor
9
+ * units exceed what a double can hold exactly above 2^53. Stored
10
+ * as a number, a balance silently changes when it is read back.
11
+ *
12
+ * SORTING a string sorts lexically, so "9" comes after "10" and every
13
+ * ORDER BY on a balance is wrong. Range filters are worse: they
14
+ * silently return the wrong rows rather than failing.
15
+ *
16
+ * So both are stored, with one of them authoritative and the other explicitly
17
+ * not.
18
+ *
19
+ * ## Why the sort key cannot be an integer
20
+ *
21
+ * The obvious answer is "store minor units as a long". It does not work. One
22
+ * ETH is 10^18 minor units and a signed 64-bit integer stops at about
23
+ * 9.22 * 10^18, so the column overflows somewhere around nine ETH. For an
24
+ * 18-decimal asset an integer sort key is not a tradeoff, it is a defect that
25
+ * appears the first time somebody holds a real balance.
26
+ *
27
+ * The sort key is therefore a DOUBLE holding the whole-unit value. It is lossy
28
+ * by design and never read as money.
29
+ *
30
+ * ## The one property the sort key must have
31
+ *
32
+ * Monotonicity: if a is less than b, then sortKey(a) must not be greater than
33
+ * sortKey(b). Equal is fine — two enormous amounts a few units apart can share
34
+ * a double, and a tie broken by the exact string costs one comparison. What is
35
+ * never allowed is an INVERSION, because that is an ORDER BY that puts a larger
36
+ * balance below a smaller one, and no downstream code can detect it.
37
+ *
38
+ * Rounding to nearest gives exactly that: it can collapse two values together
39
+ * but cannot reorder them.
40
+ */
41
+ /**
42
+ * One amount, as stored.
43
+ *
44
+ * `units` is the authority. `value` is the same number for a human and for any
45
+ * consumer that does not want to know the asset's decimals. `sort` is for the
46
+ * database and nothing else.
47
+ */
48
+ export interface StoredMoney {
49
+ /** Minor units, as a base-10 string. THE authoritative field. */
50
+ units: string;
51
+ /** The same amount in whole units, exact, e.g. `"1234.5678"`. */
52
+ value: string;
53
+ asset: string;
54
+ /**
55
+ * Whole units as a double, for ORDER BY and range filters ONLY.
56
+ *
57
+ * Never read this back as money and never compute with it. It exists so the
58
+ * database can order rows it cannot otherwise order.
59
+ */
60
+ sort: number;
61
+ }
62
+ /** The field a query sorts on, so callers do not hardcode the name. */
63
+ export declare const SORT_FIELD: "sort";
64
+ /**
65
+ * Write an amount.
66
+ *
67
+ * `value` is derived from `units` rather than carried alongside it, so the two
68
+ * cannot disagree — a row where they do is a row nobody can reconcile.
69
+ */
70
+ export declare function toStored(amount: Money): StoredMoney;
71
+ /**
72
+ * Read an amount back, exactly.
73
+ *
74
+ * From `units`, never from `sort` and never from `value`. `units` is the only
75
+ * field that survives a round trip unchanged for every asset.
76
+ */
77
+ export declare function fromStored(stored: StoredMoney): Money;
78
+ /**
79
+ * The sortable double for an amount.
80
+ *
81
+ * Round-to-nearest via `Number(string)`, which is monotonic: it can map two
82
+ * distinct amounts to one double but can never swap their order.
83
+ *
84
+ * Beyond roughly 1.8e308 a double is `Infinity`. That is still monotonic and
85
+ * still sorts correctly at the top of the range; it is only unusable as a
86
+ * value, which it already was.
87
+ */
88
+ export declare function sortKey(amount: Money): number;
89
+ /**
90
+ * Bounds for a range filter, widened so the coarse index cannot exclude a row
91
+ * that genuinely matches.
92
+ *
93
+ * A filter written directly against `sort` can drop a boundary row, because the
94
+ * stored double may sit a fraction below the true value. Widening by one ULP in
95
+ * each direction makes the database step a SUPERSET, which the caller then
96
+ * narrows exactly. A superset is recoverable; a missing row is not, because
97
+ * nothing downstream knows it should have been there.
98
+ */
99
+ export declare function rangeBounds(from: Money | null, to: Money | null): {
100
+ gte?: number;
101
+ lte?: number;
102
+ };
103
+ /** Exact comparison, for narrowing what the coarse filter returned. */
104
+ export declare function withinRange(amount: Money, from: Money | null, to: Money | null): boolean;
105
+ /**
106
+ * Sort rows exactly, breaking the ties the double could not.
107
+ *
108
+ * Used where the whole set is already in memory. For anything larger the
109
+ * database orders by `sort` and this settles the equal keys.
110
+ */
111
+ export declare function sortExact<T>(rows: readonly T[], pick: (row: T) => Money, direction?: 'asc' | 'desc'): T[];
112
+ /** Parse and store in one step — the common path from a form or an API body. */
113
+ export declare const storeAmount: (value: string, asset: string) => StoredMoney;
@@ -0,0 +1,226 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/finance/money.ts
10
+ class MoneyError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = "MoneyError";
16
+ }
17
+ }
18
+ var ASSETS = [
19
+ { code: "USDT", decimals: 6 },
20
+ { code: "USDC", decimals: 6 },
21
+ { code: "BTC", decimals: 8 },
22
+ { code: "ETH", decimals: 18 },
23
+ { code: "BNB", decimals: 18 },
24
+ { code: "EUR", decimals: 2 },
25
+ { code: "USD", decimals: 2 }
26
+ ];
27
+ var REGISTRY = new Map(ASSETS.map((asset) => [asset.code, asset]));
28
+ function defineAsset(spec) {
29
+ if (spec.decimals < 0 || spec.decimals > 30 || !Number.isInteger(spec.decimals)) {
30
+ throw new MoneyError("UNKNOWN_ASSET", `${spec.code}: decimals must be an integer 0–30.`);
31
+ }
32
+ REGISTRY.set(spec.code, spec);
33
+ }
34
+ function assetSpec(code) {
35
+ const spec = REGISTRY.get(code);
36
+ if (!spec)
37
+ throw new MoneyError("UNKNOWN_ASSET", `Unknown asset "${code}". Call defineAsset first.`);
38
+ return spec;
39
+ }
40
+ var money = (units, asset) => {
41
+ assetSpec(asset);
42
+ return { units, asset };
43
+ };
44
+ var zero = (asset) => money(0n, asset);
45
+ function parseAmount(value, asset) {
46
+ const spec = assetSpec(asset);
47
+ const text = value.trim();
48
+ if (!/^-?\d+(\.\d+)?$/.test(text)) {
49
+ throw new MoneyError("NOT_FINITE", `"${value}" is not a plain decimal amount.`);
50
+ }
51
+ const negative = text.startsWith("-");
52
+ const [whole, fraction = ""] = text.replace("-", "").split(".");
53
+ if (fraction.length > spec.decimals) {
54
+ throw new MoneyError("PRECISION_LOSS", `${asset} has ${spec.decimals} decimals; "${value}" has ${fraction.length}.`);
55
+ }
56
+ const padded = fraction.padEnd(spec.decimals, "0");
57
+ const units = BigInt(whole + padded);
58
+ return { units: negative ? -units : units, asset };
59
+ }
60
+ function formatAmount(amount, options = {}) {
61
+ const spec = assetSpec(amount.asset);
62
+ const negative = amount.units < 0n;
63
+ const digits = (negative ? -amount.units : amount.units).toString().padStart(spec.decimals + 1, "0");
64
+ const whole = digits.slice(0, digits.length - spec.decimals);
65
+ let fraction = spec.decimals === 0 ? "" : digits.slice(digits.length - spec.decimals);
66
+ if (options.trim && fraction)
67
+ fraction = fraction.replace(/0+$/, "");
68
+ return `${negative ? "-" : ""}${whole}${fraction ? `.${fraction}` : ""}`;
69
+ }
70
+ function sameAsset(a, b) {
71
+ if (a.asset !== b.asset) {
72
+ throw new MoneyError("ASSET_MISMATCH", `Cannot combine ${a.asset} and ${b.asset}.`);
73
+ }
74
+ }
75
+ function add(a, b) {
76
+ sameAsset(a, b);
77
+ return { units: a.units + b.units, asset: a.asset };
78
+ }
79
+ function subtract(a, b) {
80
+ sameAsset(a, b);
81
+ return { units: a.units - b.units, asset: a.asset };
82
+ }
83
+ var negate = (amount) => ({ units: -amount.units, asset: amount.asset });
84
+ var abs = (amount) => ({
85
+ units: amount.units < 0n ? -amount.units : amount.units,
86
+ asset: amount.asset
87
+ });
88
+ var isZero = (amount) => amount.units === 0n;
89
+ var isNegative = (amount) => amount.units < 0n;
90
+ function compare(a, b) {
91
+ sameAsset(a, b);
92
+ return a.units < b.units ? -1 : a.units > b.units ? 1 : 0;
93
+ }
94
+ var equals = (a, b) => a.asset === b.asset && a.units === b.units;
95
+ var ROUNDING = ["down", "up", "half-up"];
96
+ function divideRounded(numerator, denominator, mode) {
97
+ if (denominator === 0n)
98
+ throw new MoneyError("DIVIDE_BY_ZERO", "Division by zero.");
99
+ const negative = numerator < 0n !== denominator < 0n;
100
+ const a = numerator < 0n ? -numerator : numerator;
101
+ const b = denominator < 0n ? -denominator : denominator;
102
+ const quotient = a / b;
103
+ const remainder = a % b;
104
+ if (remainder === 0n)
105
+ return negative ? -quotient : quotient;
106
+ let result = quotient;
107
+ if (mode === "up")
108
+ result += 1n;
109
+ else if (mode === "half-up" && remainder * 2n >= b)
110
+ result += 1n;
111
+ return negative ? -result : result;
112
+ }
113
+ function mulRate(amount, rate, mode = "down") {
114
+ if (!/^-?\d+(\.\d+)?$/.test(rate.trim())) {
115
+ throw new MoneyError("NOT_FINITE", `"${rate}" is not a plain decimal rate.`);
116
+ }
117
+ const [whole, fraction = ""] = rate.trim().replace("-", "").split(".");
118
+ const scale = 10n ** BigInt(fraction.length);
119
+ const scaled = BigInt(whole + fraction) * (rate.trim().startsWith("-") ? -1n : 1n);
120
+ return { units: divideRounded(amount.units * scaled, scale, mode), asset: amount.asset };
121
+ }
122
+ function convert(amount, to, rate, mode = "down") {
123
+ const from = assetSpec(amount.asset);
124
+ const target = assetSpec(to);
125
+ const asTarget = mulRate({ units: amount.units, asset: to }, rate, mode);
126
+ const shift = target.decimals - from.decimals;
127
+ if (shift === 0)
128
+ return asTarget;
129
+ if (shift > 0)
130
+ return { units: asTarget.units * 10n ** BigInt(shift), asset: to };
131
+ return { units: divideRounded(asTarget.units, 10n ** BigInt(-shift), mode), asset: to };
132
+ }
133
+ function allocate(amount, parts) {
134
+ if (parts < 1)
135
+ throw new MoneyError("NOT_FINITE", "Cannot allocate into fewer than one part.");
136
+ const each = divideRounded(amount.units, BigInt(parts), "down");
137
+ const allocated = Array.from({ length: parts }, () => each);
138
+ let remainder = amount.units - each * BigInt(parts);
139
+ const step = remainder < 0n ? -1n : 1n;
140
+ for (let index = 0;remainder !== 0n; index = (index + 1) % parts) {
141
+ allocated[index] += step;
142
+ remainder -= step;
143
+ }
144
+ return allocated.map((units) => ({ units, asset: amount.asset }));
145
+ }
146
+ function toStep(amount, step, mode = "down") {
147
+ const stepUnits = parseAmount(step, amount.asset).units;
148
+ if (stepUnits <= 0n)
149
+ throw new MoneyError("NOT_FINITE", "A step must be positive.");
150
+ return { units: divideRounded(amount.units, stepUnits, mode) * stepUnits, asset: amount.asset };
151
+ }
152
+ var VERSION = "0.1.0";
153
+
154
+ // src/finance/storage.ts
155
+ var SORT_FIELD = "sort";
156
+ function toStored(amount) {
157
+ const value = formatAmount(amount);
158
+ return {
159
+ units: amount.units.toString(),
160
+ value,
161
+ asset: amount.asset,
162
+ sort: sortKey(amount)
163
+ };
164
+ }
165
+ function fromStored(stored) {
166
+ if (!/^-?\d+$/.test(stored.units)) {
167
+ throw new MoneyError("NOT_FINITE", `Stored units are not an integer: ${stored.units}`);
168
+ }
169
+ assetSpec(stored.asset);
170
+ return { units: BigInt(stored.units), asset: stored.asset };
171
+ }
172
+ function sortKey(amount) {
173
+ return Number(formatAmount(amount));
174
+ }
175
+ function rangeBounds(from, to) {
176
+ const bounds = {};
177
+ if (from)
178
+ bounds.gte = previousDouble(sortKey(from));
179
+ if (to)
180
+ bounds.lte = nextDouble(sortKey(to));
181
+ return bounds;
182
+ }
183
+ function withinRange(amount, from, to) {
184
+ if (from && compare(amount, from) < 0)
185
+ return false;
186
+ if (to && compare(amount, to) > 0)
187
+ return false;
188
+ return true;
189
+ }
190
+ function sortExact(rows, pick, direction = "asc") {
191
+ const sign = direction === "asc" ? 1 : -1;
192
+ return [...rows].sort((a, b) => sign * compare(pick(a), pick(b)));
193
+ }
194
+ var storeAmount = (value, asset) => toStored(parseAmount(value, asset));
195
+ var nextDouble = (value) => {
196
+ if (!Number.isFinite(value))
197
+ return value;
198
+ if (value === 0)
199
+ return Number.MIN_VALUE;
200
+ const buffer = new DataView(new ArrayBuffer(8));
201
+ buffer.setFloat64(0, value);
202
+ const bits = buffer.getBigUint64(0);
203
+ buffer.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n);
204
+ return buffer.getFloat64(0);
205
+ };
206
+ var previousDouble = (value) => {
207
+ if (!Number.isFinite(value))
208
+ return value;
209
+ if (value === 0)
210
+ return -Number.MIN_VALUE;
211
+ const buffer = new DataView(new ArrayBuffer(8));
212
+ buffer.setFloat64(0, value);
213
+ const bits = buffer.getBigUint64(0);
214
+ buffer.setBigUint64(0, value > 0 ? bits - 1n : bits + 1n);
215
+ return buffer.getFloat64(0);
216
+ };
217
+ export {
218
+ withinRange,
219
+ toStored,
220
+ storeAmount,
221
+ sortKey,
222
+ sortExact,
223
+ rangeBounds,
224
+ fromStored,
225
+ SORT_FIELD
226
+ };