@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,596 @@
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/ledger.ts
155
+ class LedgerError extends Error {
156
+ code;
157
+ constructor(code, message) {
158
+ super(message);
159
+ this.code = code;
160
+ this.name = "LedgerError";
161
+ }
162
+ }
163
+ var BUCKETS = ["available", "held"];
164
+ var ACCOUNT_KINDS = ["user", "platform", "external"];
165
+ var accountId = (account) => `${account.kind}:${account.owner}:${account.bucket ?? "available"}`;
166
+ function parseAccount(id) {
167
+ const [kind, owner, bucket] = id.split(":");
168
+ return { kind, owner, bucket };
169
+ }
170
+ var queueKeyFor = (account) => `${account.kind}:${account.owner}`;
171
+ function assertBalanced(transaction) {
172
+ if (transaction.entries.length === 0) {
173
+ throw new LedgerError("EMPTY_TRANSACTION", "A transaction needs at least two entries.");
174
+ }
175
+ const totals = new Map;
176
+ for (const entry of transaction.entries) {
177
+ if (entry.amount.units === 0n) {
178
+ throw new LedgerError("ZERO_ENTRY", `An entry on ${accountId(entry.account)} moves nothing. Remove it or give it an amount.`);
179
+ }
180
+ totals.set(entry.amount.asset, (totals.get(entry.amount.asset) ?? 0n) + entry.amount.units);
181
+ }
182
+ for (const [asset, total] of totals) {
183
+ if (total !== 0n) {
184
+ throw new LedgerError("UNBALANCED", `${asset} does not sum to zero — it is off by ${total} minor units. Every movement needs a counterparty.`);
185
+ }
186
+ }
187
+ }
188
+ function transfer(args) {
189
+ const transaction = {
190
+ reference: args.reference,
191
+ kind: args.kind,
192
+ atMs: args.atMs,
193
+ memo: args.memo,
194
+ entries: [
195
+ { account: args.from, amount: { units: -args.amount.units, asset: args.amount.asset } },
196
+ { account: args.to, amount: args.amount }
197
+ ]
198
+ };
199
+ assertBalanced(transaction);
200
+ return transaction;
201
+ }
202
+ var placeHold = (args) => transfer({
203
+ reference: args.reference,
204
+ kind: "hold.place",
205
+ from: { kind: args.kind ?? "user", owner: args.owner, bucket: "available" },
206
+ to: { kind: args.kind ?? "user", owner: args.owner, bucket: "held" },
207
+ amount: args.amount,
208
+ atMs: args.atMs,
209
+ memo: args.memo
210
+ });
211
+ var releaseHold = (args) => transfer({
212
+ reference: args.reference,
213
+ kind: "hold.release",
214
+ from: { kind: args.kind ?? "user", owner: args.owner, bucket: "held" },
215
+ to: { kind: args.kind ?? "user", owner: args.owner, bucket: "available" },
216
+ amount: args.amount,
217
+ atMs: args.atMs
218
+ });
219
+ var captureHold = (args) => transfer({
220
+ reference: args.reference,
221
+ kind: "hold.capture",
222
+ from: { kind: args.kind ?? "user", owner: args.owner, bucket: "held" },
223
+ to: args.to,
224
+ amount: args.amount,
225
+ atMs: args.atMs,
226
+ memo: args.memo
227
+ });
228
+ function balancesFrom(transactions) {
229
+ const balances = new Map;
230
+ for (const transaction of transactions) {
231
+ for (const entry of transaction.entries) {
232
+ const id = accountId(entry.account);
233
+ const perAsset = balances.get(id) ?? new Map;
234
+ const current = perAsset.get(entry.amount.asset) ?? zero(entry.amount.asset);
235
+ perAsset.set(entry.amount.asset, add(current, entry.amount));
236
+ balances.set(id, perAsset);
237
+ }
238
+ }
239
+ return balances;
240
+ }
241
+ function balanceOf(balances, account, asset) {
242
+ return balances.get(accountId(account))?.get(asset) ?? zero(asset);
243
+ }
244
+ var availableOf = (balances, owner, asset, kind = "user") => balanceOf(balances, { kind, owner, bucket: "available" }, asset);
245
+ var heldOf = (balances, owner, asset, kind = "user") => balanceOf(balances, { kind, owner, bucket: "held" }, asset);
246
+ var totalOf = (balances, owner, asset, kind = "user") => add(availableOf(balances, owner, asset, kind), heldOf(balances, owner, asset, kind));
247
+ function trialBalance(transactions) {
248
+ const totals = new Map;
249
+ const accounts = new Map;
250
+ for (const transaction of transactions) {
251
+ for (const entry of transaction.entries) {
252
+ totals.set(entry.amount.asset, (totals.get(entry.amount.asset) ?? 0n) + entry.amount.units);
253
+ const seen = accounts.get(entry.amount.asset) ?? new Set;
254
+ seen.add(accountId(entry.account));
255
+ accounts.set(entry.amount.asset, seen);
256
+ }
257
+ }
258
+ const perAsset = [...totals.entries()].map(([asset, total]) => ({
259
+ asset,
260
+ total,
261
+ accounts: accounts.get(asset)?.size ?? 0
262
+ }));
263
+ const discrepancies = perAsset.filter((row) => row.total !== 0n).map((row) => ({ asset: row.asset, off: row.total }));
264
+ return { ok: discrepancies.length === 0, perAsset, discrepancies };
265
+ }
266
+ function statement(transactions, account, asset) {
267
+ const id = accountId(account);
268
+ let running = zero(asset);
269
+ return [...transactions].sort((a, b) => a.atMs - b.atMs).flatMap((transaction) => transaction.entries.filter((entry) => accountId(entry.account) === id && entry.amount.asset === asset).map((entry) => {
270
+ running = add(running, entry.amount);
271
+ return {
272
+ atMs: transaction.atMs,
273
+ reference: transaction.reference,
274
+ kind: transaction.kind,
275
+ amount: formatAmount(entry.amount, { trim: true }),
276
+ balance: formatAmount(running, { trim: true })
277
+ };
278
+ }));
279
+ }
280
+ function assertAvailable(balances, args) {
281
+ const available = availableOf(balances, args.owner, args.amount.asset, args.kind ?? "user");
282
+ if (subtract(available, args.amount).units < 0n) {
283
+ throw new LedgerError("INSUFFICIENT_AVAILABLE", `${args.owner} has ${formatAmount(available, { trim: true })} ${args.amount.asset} available; this needs ${formatAmount(args.amount, { trim: true })}.`);
284
+ }
285
+ }
286
+ var VERSION2 = "0.1.0";
287
+
288
+ // src/finance/chain.ts
289
+ class ChainError extends Error {
290
+ code;
291
+ constructor(code, message) {
292
+ super(message);
293
+ this.code = code;
294
+ this.name = "ChainError";
295
+ }
296
+ }
297
+ var ADDRESS_SCHEMES = ["evm", "bitcoin", "tron", "solana"];
298
+ var SCHEME_PATTERNS = {
299
+ evm: /^0x[a-fA-F0-9]{40}$/,
300
+ bitcoin: /^(bc1[a-z0-9]{25,62}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})$/,
301
+ tron: /^T[a-zA-Z0-9]{33}$/,
302
+ solana: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/
303
+ };
304
+ function validateAddress(address, chain) {
305
+ if (!SCHEME_PATTERNS[chain.scheme].test(address.trim())) {
306
+ throw new ChainError("BAD_ADDRESS", `"${address.slice(0, 12)}…" is not a valid ${chain.label} address. Check you have not pasted an address for a different chain — that is the mistake that cannot be undone.`);
307
+ }
308
+ }
309
+ function assertChain(chain) {
310
+ if (chain.confirmations.length === 0) {
311
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} declares no confirmation depth.`);
312
+ }
313
+ if (chain.confirmations[0].aboveUsd !== 0) {
314
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} must have a band starting at 0, or small transfers match no rule.`);
315
+ }
316
+ let previous = -1;
317
+ for (const band of chain.confirmations) {
318
+ if (band.aboveUsd <= previous) {
319
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} bands must ascend; ${band.aboveUsd} follows ${previous}.`);
320
+ }
321
+ if (!Number.isInteger(band.confirmations) || band.confirmations < 1) {
322
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} band above $${band.aboveUsd} needs at least one confirmation.`);
323
+ }
324
+ previous = band.aboveUsd;
325
+ }
326
+ }
327
+ function confirmationsFor(chain, usdValue) {
328
+ let depth = chain.confirmations[0].confirmations;
329
+ for (const band of chain.confirmations) {
330
+ if (usdValue >= band.aboveUsd)
331
+ depth = band.confirmations;
332
+ }
333
+ return depth;
334
+ }
335
+ var estimatedSeconds = (chain, usdValue) => confirmationsFor(chain, usdValue) * chain.blockSeconds;
336
+ function createChainRegistry(args) {
337
+ for (const chain of args.chains)
338
+ assertChain(chain);
339
+ const chains = new Map(args.chains.map((chain) => [chain.key, chain]));
340
+ return {
341
+ all: () => [...chains.values()].filter((chain) => chain.enabled),
342
+ byKey(key) {
343
+ const chain = chains.get(key);
344
+ if (!chain)
345
+ throw new ChainError("UNKNOWN_CHAIN", `No chain "${key}" is configured.`);
346
+ return chain;
347
+ },
348
+ assetsOn: (chain) => args.assets.filter((asset) => asset.chain === chain && asset.enabled),
349
+ chainsFor: (asset) => args.assets.filter((entry) => entry.asset === asset && entry.enabled).map((entry) => chains.get(entry.chain)).filter((chain) => chain?.enabled === true),
350
+ asset(chain, asset) {
351
+ const entry = args.assets.find((row) => row.chain === chain && row.asset === asset);
352
+ if (!entry) {
353
+ throw new ChainError("UNKNOWN_ASSET", `${asset} is not configured on ${chain}.`);
354
+ }
355
+ assetSpec(asset);
356
+ return entry;
357
+ },
358
+ explorerTx: (chain, hash) => {
359
+ const spec = chains.get(chain);
360
+ return spec ? `${spec.explorerTx}${hash}` : hash;
361
+ },
362
+ explorerAddress: (chain, address) => {
363
+ const spec = chains.get(chain);
364
+ return spec ? `${spec.explorerAddress}${address}` : address;
365
+ },
366
+ validate(chain, address) {
367
+ validateAddress(address, this.byKey(chain));
368
+ }
369
+ };
370
+ }
371
+ var SEED_CHAINS = [
372
+ {
373
+ key: "bsc",
374
+ label: "BNB Smart Chain",
375
+ scheme: "evm",
376
+ chainId: 56,
377
+ nativeAsset: "BNB",
378
+ blockSeconds: 3,
379
+ confirmations: [
380
+ { aboveUsd: 0, confirmations: 15 },
381
+ { aboveUsd: 1e4, confirmations: 30 },
382
+ { aboveUsd: 1e5, confirmations: 60 }
383
+ ],
384
+ explorerTx: "https://bscscan.com/tx/",
385
+ explorerAddress: "https://bscscan.com/address/",
386
+ enabled: true
387
+ },
388
+ {
389
+ key: "ethereum",
390
+ label: "Ethereum",
391
+ scheme: "evm",
392
+ chainId: 1,
393
+ nativeAsset: "ETH",
394
+ blockSeconds: 12,
395
+ confirmations: [
396
+ { aboveUsd: 0, confirmations: 12 },
397
+ { aboveUsd: 50000, confirmations: 32 }
398
+ ],
399
+ explorerTx: "https://etherscan.io/tx/",
400
+ explorerAddress: "https://etherscan.io/address/",
401
+ enabled: true
402
+ },
403
+ {
404
+ key: "tron",
405
+ label: "Tron",
406
+ scheme: "tron",
407
+ nativeAsset: "TRX",
408
+ blockSeconds: 3,
409
+ confirmations: [{ aboveUsd: 0, confirmations: 20 }],
410
+ explorerTx: "https://tronscan.org/#/transaction/",
411
+ explorerAddress: "https://tronscan.org/#/address/",
412
+ enabled: true
413
+ }
414
+ ];
415
+ var SEED_CHAIN_ASSETS = [
416
+ { chain: "bsc", asset: "BNB", minDeposit: "0.001", enabled: true },
417
+ {
418
+ chain: "bsc",
419
+ asset: "USDT",
420
+ contract: "0x55d398326f99059ff775485246999027b3197955",
421
+ minDeposit: "1",
422
+ enabled: true
423
+ },
424
+ { chain: "ethereum", asset: "ETH", minDeposit: "0.005", enabled: true },
425
+ {
426
+ chain: "ethereum",
427
+ asset: "USDT",
428
+ contract: "0xdac17f958d2ee523a2206206994597c13d831ec7",
429
+ minDeposit: "20",
430
+ enabled: true
431
+ },
432
+ {
433
+ chain: "tron",
434
+ asset: "USDT",
435
+ contract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
436
+ minDeposit: "1",
437
+ enabled: true
438
+ }
439
+ ];
440
+
441
+ // src/finance/chain-deposits.ts
442
+ class DepositError extends Error {
443
+ code;
444
+ constructor(code, message) {
445
+ super(message);
446
+ this.code = code;
447
+ this.name = "DepositError";
448
+ }
449
+ }
450
+ var depositReference = (args) => `deposit:${args.chain}:${args.txHash.toLowerCase()}:${args.logIndex}`;
451
+ function isConfirmed(args) {
452
+ const depth = args.head - args.transfer.blockNumber + 1;
453
+ return depth >= confirmationsFor(args.chain, args.usdValue);
454
+ }
455
+ function creditDeposit(args) {
456
+ const external = { kind: "external", owner: args.chain, bucket: "available" };
457
+ const user = { kind: "user", owner: args.owner, bucket: "available" };
458
+ return transfer({
459
+ reference: depositReference({
460
+ chain: args.chain,
461
+ txHash: args.transfer.txHash,
462
+ logIndex: args.transfer.logIndex
463
+ }),
464
+ kind: "deposit",
465
+ from: external,
466
+ to: user,
467
+ amount: args.amount,
468
+ atMs: args.atMs,
469
+ memo: `${args.chain} ${args.transfer.txHash}`
470
+ });
471
+ }
472
+ var DEFAULT_BATCH = 2000;
473
+ async function scanOnce(cursor, context) {
474
+ if (cursor.chain !== context.chain.key) {
475
+ throw new DepositError("BAD_CURSOR", `That cursor is for ${cursor.chain}, not ${context.chain.key}.`);
476
+ }
477
+ const head = await context.reader.head();
478
+ const fromBlock = cursor.scannedTo + 1;
479
+ const batch = Math.max(1, context.batchSize ?? DEFAULT_BATCH);
480
+ const toBlock = Math.min(head, cursor.scannedTo + batch);
481
+ if (toBlock < fromBlock) {
482
+ return {
483
+ chain: context.chain.key,
484
+ fromBlock,
485
+ toBlock: cursor.scannedTo,
486
+ head,
487
+ matched: 0,
488
+ credited: 0,
489
+ pending: 0,
490
+ duplicates: 0,
491
+ dust: 0,
492
+ transactions: [],
493
+ cursor
494
+ };
495
+ }
496
+ const found = await context.reader.transfersIn({ fromBlock, toBlock });
497
+ const outcome = await classify(found, context, head);
498
+ const shallowest = outcome.pendingBlocks.length > 0 ? Math.min(...outcome.pendingBlocks) : null;
499
+ const scannedTo = shallowest === null ? toBlock : shallowest - 1;
500
+ return {
501
+ chain: context.chain.key,
502
+ fromBlock,
503
+ toBlock,
504
+ head,
505
+ matched: outcome.matched,
506
+ credited: outcome.transactions.length,
507
+ pending: outcome.pendingBlocks.length,
508
+ duplicates: outcome.duplicates,
509
+ dust: outcome.dust,
510
+ transactions: outcome.transactions,
511
+ cursor: { chain: cursor.chain, scannedTo: Math.max(cursor.scannedTo, scannedTo) }
512
+ };
513
+ }
514
+ async function rescanRange(range, context) {
515
+ if (range.toBlock < range.fromBlock) {
516
+ throw new DepositError("BAD_RANGE", "A rescan range ends before it starts.");
517
+ }
518
+ const head = await context.reader.head();
519
+ const found = await context.reader.transfersIn(range);
520
+ const outcome = await classify(found, context, head);
521
+ return {
522
+ chain: context.chain.key,
523
+ fromBlock: range.fromBlock,
524
+ toBlock: range.toBlock,
525
+ head,
526
+ matched: outcome.matched,
527
+ credited: outcome.transactions.length,
528
+ pending: outcome.pendingBlocks.length,
529
+ duplicates: outcome.duplicates,
530
+ dust: outcome.dust,
531
+ transactions: outcome.transactions,
532
+ cursor: { chain: context.chain.key, scannedTo: range.toBlock }
533
+ };
534
+ }
535
+ async function classify(found, context, head) {
536
+ const transactions = [];
537
+ const pendingBlocks = [];
538
+ let matched = 0;
539
+ let duplicates = 0;
540
+ let dust = 0;
541
+ for (const item of found) {
542
+ const owner = context.addresses.get(item.to.trim().toLowerCase());
543
+ if (!owner)
544
+ continue;
545
+ matched += 1;
546
+ let amount;
547
+ try {
548
+ amount = money(BigInt(item.units), item.asset);
549
+ } catch {
550
+ throw new DepositError("UNKNOWN_ASSET", `${item.asset} arrived at a watched address on ${context.chain.key} and is not a known asset. ` + "Crediting it would invent a balance; ignoring it silently would lose a real deposit.");
551
+ }
552
+ const usdValue = context.usdValueOf(amount);
553
+ if (context.minDepositUsd !== undefined && usdValue < context.minDepositUsd) {
554
+ dust += 1;
555
+ continue;
556
+ }
557
+ if (!isConfirmed({ chain: context.chain, transfer: item, head, usdValue })) {
558
+ pendingBlocks.push(item.blockNumber);
559
+ continue;
560
+ }
561
+ const reference = depositReference({
562
+ chain: context.chain.key,
563
+ txHash: item.txHash,
564
+ logIndex: item.logIndex
565
+ });
566
+ if (await context.seen(reference)) {
567
+ duplicates += 1;
568
+ continue;
569
+ }
570
+ transactions.push(creditDeposit({
571
+ chain: context.chain.key,
572
+ transfer: item,
573
+ owner,
574
+ amount,
575
+ atMs: context.atMs
576
+ }));
577
+ }
578
+ return { transactions, pendingBlocks, matched, duplicates, dust };
579
+ }
580
+ var cursorFrom = (chain, block) => {
581
+ if (!Number.isInteger(block) || block < 0) {
582
+ throw new DepositError("BAD_CURSOR", "A cursor starts at a whole, non-negative block.");
583
+ }
584
+ return { chain, scannedTo: block };
585
+ };
586
+ var describeScan = (report) => `${report.chain} ${report.fromBlock}–${report.toBlock} of ${report.head}: ` + `${report.credited} credited, ${report.pending} pending, ${report.duplicates} already seen, ${report.dust} dust`;
587
+ export {
588
+ scanOnce,
589
+ rescanRange,
590
+ isConfirmed,
591
+ describeScan,
592
+ depositReference,
593
+ cursorFrom,
594
+ creditDeposit,
595
+ DepositError
596
+ };
@@ -0,0 +1,112 @@
1
+ import { type Money } from './money';
2
+ /**
3
+ * Comparing what the chain holds against what the ledger says it owes.
4
+ *
5
+ * Every other guard in this package is preventive: the confirmation depth, the
6
+ * idempotency key, the hold placed with acceptance. This is the one that tells
7
+ * you a preventive guard FAILED — and it is the difference between finding a
8
+ * shortfall in a scheduled report and finding it in a withdrawal queue, where
9
+ * the person who finds it is a customer.
10
+ *
11
+ * ## Why a surplus is a finding too
12
+ *
13
+ * The instinct is to alert on "less on chain than we owe" and treat the other
14
+ * direction as a happy accident. It is not. A surplus means money arrived that
15
+ * nothing credited — a deposit the scanner missed, an asset nobody configured,
16
+ * a transfer to an address the book no longer derives. Somebody is owed that
17
+ * money and does not know it, and the longer it sits the harder it is to
18
+ * attribute. Both directions are reported, with the reason each matters.
19
+ *
20
+ * ## Why this holds no I/O
21
+ *
22
+ * `@forgezero/runtime` has no credentials and no network. Both sides are passed
23
+ * in, so the same comparison runs against a live node, a snapshot taken during
24
+ * an incident, or a fixture — and the tests exercise real discrepancies rather
25
+ * than a mock of a reconciler.
26
+ */
27
+ export declare class ReconcileError extends Error {
28
+ readonly code: 'ASSET_MISMATCH' | 'MALFORMED';
29
+ constructor(code: 'ASSET_MISMATCH' | 'MALFORMED', message: string);
30
+ }
31
+ /** What the chain actually holds, per asset, across every address in custody. */
32
+ export interface OnChainHolding {
33
+ chain: string;
34
+ asset: string;
35
+ /** Deposit addresses plus the hot wallet plus cold storage — everything. */
36
+ units: bigint;
37
+ }
38
+ /** What the ledger says is owed, per asset. */
39
+ export interface LedgerLiability {
40
+ chain: string;
41
+ asset: string;
42
+ /**
43
+ * Every user balance in this asset, available and held together.
44
+ *
45
+ * Held is INCLUDED. A withdrawal awaiting approval is money the platform
46
+ * still owes somebody; excluding it would make the platform look solvent
47
+ * exactly while a payout queue is building up against it.
48
+ */
49
+ units: bigint;
50
+ /** Dispatched and not yet confirmed — in flight, and no longer on chain. */
51
+ inFlightUnits?: bigint;
52
+ }
53
+ export declare const DISCREPANCY_KINDS: readonly ["shortfall", "surplus"];
54
+ export type DiscrepancyKind = (typeof DISCREPANCY_KINDS)[number];
55
+ export interface Discrepancy {
56
+ chain: string;
57
+ asset: string;
58
+ kind: DiscrepancyKind;
59
+ /** Always positive — `kind` carries the direction. */
60
+ difference: Money;
61
+ onChain: Money;
62
+ owed: Money;
63
+ /** What this means, in the terms an operator has to act on. */
64
+ meaning: string;
65
+ }
66
+ export interface ReconcileReport {
67
+ atMs: number;
68
+ /** Every (chain, asset) pair compared, whether or not it differed. */
69
+ checked: number;
70
+ discrepancies: readonly Discrepancy[];
71
+ /** True when nothing differed. The only result that needs no action. */
72
+ balanced: boolean;
73
+ }
74
+ /**
75
+ * Compare one snapshot of both sides.
76
+ *
77
+ * A pair present on one side and absent from the other is treated as zero on
78
+ * the missing side rather than skipped. Skipping is how the most serious case
79
+ * hides: an asset that exists on chain and is entirely unknown to the ledger
80
+ * has no ledger row to iterate over, so a loop driven by ledger rows never
81
+ * looks at it.
82
+ */
83
+ export declare function reconcileOnce(args: {
84
+ onChain: readonly OnChainHolding[];
85
+ owed: readonly LedgerLiability[];
86
+ /**
87
+ * How much difference is not worth reporting, per asset in smallest units.
88
+ *
89
+ * Not a way to hide a shortfall — a tolerance covers rounding on chains that
90
+ * charge fees in the transferred asset, and nothing larger. It is per asset
91
+ * because "a thousand units" is dust in one and a fortune in another.
92
+ */
93
+ tolerance?: Record<string, bigint>;
94
+ atMs: number;
95
+ }): ReconcileReport;
96
+ /**
97
+ * The report, as an operator reads it.
98
+ *
99
+ * Shortfalls first and named individually. A summary that leads with "3
100
+ * discrepancies" buries the one that matters underneath two that do not, and
101
+ * this text exists to be the first line of an alert.
102
+ */
103
+ export declare function reconcileReport(report: ReconcileReport): string;
104
+ /**
105
+ * The one number an operator watches.
106
+ *
107
+ * Total shortfall across every pair, in USD, using a rate function the caller
108
+ * supplies. Per-asset figures are what you act on; this is what tells you
109
+ * whether to act now — and summing units across assets without converting
110
+ * would add satoshis to cents and produce a number that means nothing.
111
+ */
112
+ export declare function shortfallUsd(report: ReconcileReport, usdValueOf: (amount: Money) => number): number;