@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,462 @@
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/custody.ts
155
+ import { keccak_256 } from "@noble/hashes/sha3.js";
156
+ var hex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
157
+ var bytes = (value) => {
158
+ const bare = value.replace(/^0x/i, "");
159
+ if (bare.length % 2)
160
+ throw new CustodyError("MALFORMED", `Not byte-aligned: ${value}`);
161
+ const out = new Uint8Array(bare.length / 2);
162
+ for (let i = 0;i < out.length; i++)
163
+ out[i] = parseInt(bare.slice(i * 2, i * 2 + 2), 16);
164
+ return out;
165
+ };
166
+ var concat = (...parts) => {
167
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
168
+ let at = 0;
169
+ for (const part of parts) {
170
+ out.set(part, at);
171
+ at += part.length;
172
+ }
173
+ return out;
174
+ };
175
+
176
+ class CustodyError extends Error {
177
+ code;
178
+ constructor(code, message) {
179
+ super(message);
180
+ this.code = code;
181
+ this.name = "CustodyError";
182
+ }
183
+ }
184
+ var assertAddress = (value, what) => {
185
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
186
+ throw new CustodyError("NOT_AN_ADDRESS", `${what} is not an address: ${value}`);
187
+ }
188
+ return value;
189
+ };
190
+ function toChecksumAddress(address) {
191
+ const bare = assertAddress(address, "address").slice(2).toLowerCase();
192
+ const digest = hex(keccak_256(new TextEncoder().encode(bare)));
193
+ let out = "0x";
194
+ for (let i = 0;i < bare.length; i++) {
195
+ out += parseInt(digest[i], 16) >= 8 ? bare[i].toUpperCase() : bare[i];
196
+ }
197
+ return out;
198
+ }
199
+ function saltFor(namespace, key) {
200
+ return `0x${hex(keccak_256(new TextEncoder().encode(`${namespace}:${key}`)))}`;
201
+ }
202
+ function depositAddress(args) {
203
+ assertAddress(args.factory, "factory");
204
+ const salt = bytes(args.salt);
205
+ if (salt.length !== 32)
206
+ throw new CustodyError("MALFORMED", "A salt is 32 bytes.");
207
+ const initCodeHash = bytes(args.proxyInitCodeHash);
208
+ if (initCodeHash.length !== 32)
209
+ throw new CustodyError("MALFORMED", "An init code hash is 32 bytes.");
210
+ const digest = keccak_256(concat(Uint8Array.of(255), bytes(args.factory), salt, initCodeHash));
211
+ return toChecksumAddress(`0x${hex(digest.slice(12))}`);
212
+ }
213
+ var keccakText = (value) => keccak_256(new TextEncoder().encode(value));
214
+ var word = (value) => {
215
+ const raw = typeof value === "bigint" ? value.toString(16) : value.replace(/^0x/i, "");
216
+ return bytes(raw.padStart(64, "0"));
217
+ };
218
+ var DOMAIN_TYPEHASH = keccakText("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
219
+ var WITHDRAW_TYPEHASH = keccakText("Withdraw(address token,address to,uint256 amount,uint256 nonce)");
220
+ function domainSeparator(args) {
221
+ assertAddress(args.vault, "vault");
222
+ return `0x${hex(keccak_256(concat(DOMAIN_TYPEHASH, keccakText("ForgeZeroColdVault"), keccakText("1"), word(args.chainId), word(args.vault))))}`;
223
+ }
224
+ function withdrawDigest(args) {
225
+ assertAddress(args.to, "to");
226
+ assertAddress(args.token, "token");
227
+ const structHash = keccak_256(concat(WITHDRAW_TYPEHASH, word(args.token), word(args.to), word(args.amount), word(args.nonce)));
228
+ const separator = bytes(domainSeparator({ chainId: args.chainId, vault: args.vault }));
229
+ return `0x${hex(keccak_256(concat(Uint8Array.of(25, 1), separator, structHash)))}`;
230
+ }
231
+ function orderSignatures(signatures) {
232
+ const seen = new Set;
233
+ for (const entry of signatures) {
234
+ const key = assertAddress(entry.signer, "signer").toLowerCase();
235
+ if (seen.has(key)) {
236
+ throw new CustodyError("BAD_THRESHOLD", `${entry.signer} signed twice; that is one approval.`);
237
+ }
238
+ seen.add(key);
239
+ }
240
+ return [...signatures].sort((a, b) => BigInt(a.signer) < BigInt(b.signer) ? -1 : 1).map((entry) => entry.signature);
241
+ }
242
+ var VERSION2 = "0.1.0";
243
+
244
+ // src/finance/chain.ts
245
+ class ChainError extends Error {
246
+ code;
247
+ constructor(code, message) {
248
+ super(message);
249
+ this.code = code;
250
+ this.name = "ChainError";
251
+ }
252
+ }
253
+ var ADDRESS_SCHEMES = ["evm", "bitcoin", "tron", "solana"];
254
+ var SCHEME_PATTERNS = {
255
+ evm: /^0x[a-fA-F0-9]{40}$/,
256
+ bitcoin: /^(bc1[a-z0-9]{25,62}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})$/,
257
+ tron: /^T[a-zA-Z0-9]{33}$/,
258
+ solana: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/
259
+ };
260
+ function validateAddress(address, chain) {
261
+ if (!SCHEME_PATTERNS[chain.scheme].test(address.trim())) {
262
+ 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.`);
263
+ }
264
+ }
265
+ function assertChain(chain) {
266
+ if (chain.confirmations.length === 0) {
267
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} declares no confirmation depth.`);
268
+ }
269
+ if (chain.confirmations[0].aboveUsd !== 0) {
270
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} must have a band starting at 0, or small transfers match no rule.`);
271
+ }
272
+ let previous = -1;
273
+ for (const band of chain.confirmations) {
274
+ if (band.aboveUsd <= previous) {
275
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} bands must ascend; ${band.aboveUsd} follows ${previous}.`);
276
+ }
277
+ if (!Number.isInteger(band.confirmations) || band.confirmations < 1) {
278
+ throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} band above $${band.aboveUsd} needs at least one confirmation.`);
279
+ }
280
+ previous = band.aboveUsd;
281
+ }
282
+ }
283
+ function confirmationsFor(chain, usdValue) {
284
+ let depth = chain.confirmations[0].confirmations;
285
+ for (const band of chain.confirmations) {
286
+ if (usdValue >= band.aboveUsd)
287
+ depth = band.confirmations;
288
+ }
289
+ return depth;
290
+ }
291
+ var estimatedSeconds = (chain, usdValue) => confirmationsFor(chain, usdValue) * chain.blockSeconds;
292
+ function createChainRegistry(args) {
293
+ for (const chain of args.chains)
294
+ assertChain(chain);
295
+ const chains = new Map(args.chains.map((chain) => [chain.key, chain]));
296
+ return {
297
+ all: () => [...chains.values()].filter((chain) => chain.enabled),
298
+ byKey(key) {
299
+ const chain = chains.get(key);
300
+ if (!chain)
301
+ throw new ChainError("UNKNOWN_CHAIN", `No chain "${key}" is configured.`);
302
+ return chain;
303
+ },
304
+ assetsOn: (chain) => args.assets.filter((asset) => asset.chain === chain && asset.enabled),
305
+ chainsFor: (asset) => args.assets.filter((entry) => entry.asset === asset && entry.enabled).map((entry) => chains.get(entry.chain)).filter((chain) => chain?.enabled === true),
306
+ asset(chain, asset) {
307
+ const entry = args.assets.find((row) => row.chain === chain && row.asset === asset);
308
+ if (!entry) {
309
+ throw new ChainError("UNKNOWN_ASSET", `${asset} is not configured on ${chain}.`);
310
+ }
311
+ assetSpec(asset);
312
+ return entry;
313
+ },
314
+ explorerTx: (chain, hash) => {
315
+ const spec = chains.get(chain);
316
+ return spec ? `${spec.explorerTx}${hash}` : hash;
317
+ },
318
+ explorerAddress: (chain, address) => {
319
+ const spec = chains.get(chain);
320
+ return spec ? `${spec.explorerAddress}${address}` : address;
321
+ },
322
+ validate(chain, address) {
323
+ validateAddress(address, this.byKey(chain));
324
+ }
325
+ };
326
+ }
327
+ var SEED_CHAINS = [
328
+ {
329
+ key: "bsc",
330
+ label: "BNB Smart Chain",
331
+ scheme: "evm",
332
+ chainId: 56,
333
+ nativeAsset: "BNB",
334
+ blockSeconds: 3,
335
+ confirmations: [
336
+ { aboveUsd: 0, confirmations: 15 },
337
+ { aboveUsd: 1e4, confirmations: 30 },
338
+ { aboveUsd: 1e5, confirmations: 60 }
339
+ ],
340
+ explorerTx: "https://bscscan.com/tx/",
341
+ explorerAddress: "https://bscscan.com/address/",
342
+ enabled: true
343
+ },
344
+ {
345
+ key: "ethereum",
346
+ label: "Ethereum",
347
+ scheme: "evm",
348
+ chainId: 1,
349
+ nativeAsset: "ETH",
350
+ blockSeconds: 12,
351
+ confirmations: [
352
+ { aboveUsd: 0, confirmations: 12 },
353
+ { aboveUsd: 50000, confirmations: 32 }
354
+ ],
355
+ explorerTx: "https://etherscan.io/tx/",
356
+ explorerAddress: "https://etherscan.io/address/",
357
+ enabled: true
358
+ },
359
+ {
360
+ key: "tron",
361
+ label: "Tron",
362
+ scheme: "tron",
363
+ nativeAsset: "TRX",
364
+ blockSeconds: 3,
365
+ confirmations: [{ aboveUsd: 0, confirmations: 20 }],
366
+ explorerTx: "https://tronscan.org/#/transaction/",
367
+ explorerAddress: "https://tronscan.org/#/address/",
368
+ enabled: true
369
+ }
370
+ ];
371
+ var SEED_CHAIN_ASSETS = [
372
+ { chain: "bsc", asset: "BNB", minDeposit: "0.001", enabled: true },
373
+ {
374
+ chain: "bsc",
375
+ asset: "USDT",
376
+ contract: "0x55d398326f99059ff775485246999027b3197955",
377
+ minDeposit: "1",
378
+ enabled: true
379
+ },
380
+ { chain: "ethereum", asset: "ETH", minDeposit: "0.005", enabled: true },
381
+ {
382
+ chain: "ethereum",
383
+ asset: "USDT",
384
+ contract: "0xdac17f958d2ee523a2206206994597c13d831ec7",
385
+ minDeposit: "20",
386
+ enabled: true
387
+ },
388
+ {
389
+ chain: "tron",
390
+ asset: "USDT",
391
+ contract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
392
+ minDeposit: "1",
393
+ enabled: true
394
+ }
395
+ ];
396
+
397
+ // src/finance/chain-addresses.ts
398
+ var COUNTERFACTUAL_SCHEMES = ["evm"];
399
+
400
+ class AddressError extends Error {
401
+ code;
402
+ constructor(code, message) {
403
+ super(message);
404
+ this.code = code;
405
+ this.name = "AddressError";
406
+ }
407
+ }
408
+ var DEPOSIT_NAMESPACE = "deposit";
409
+ var depositSaltFor = (args) => saltFor(DEPOSIT_NAMESPACE, `${args.chain}:${args.owner}`);
410
+ function deriveDepositAddress(args) {
411
+ if (!args.owner?.trim()) {
412
+ throw new AddressError("MALFORMED", "An owner key is required to derive an address.");
413
+ }
414
+ if (!COUNTERFACTUAL_SCHEMES.includes(args.chain.scheme)) {
415
+ throw new AddressError("SCHEME_UNSUPPORTED", `${args.chain.label} uses ${args.chain.scheme} addressing, which has no counterfactual derivation here. ` + "Deriving one with the EVM formula would produce an address on no chain at all.");
416
+ }
417
+ if (!args.chain.enabled) {
418
+ throw new AddressError("CHAIN_DISABLED", `${args.chain.label} is not accepting deposits.`);
419
+ }
420
+ if (args.deployment.chain !== args.chain.key) {
421
+ throw new AddressError("MISSING_DEPLOYMENT", `That deployment belongs to ${args.deployment.chain}, not ${args.chain.key}.`);
422
+ }
423
+ try {
424
+ return depositAddress({
425
+ factory: args.deployment.factory,
426
+ salt: depositSaltFor({ chain: args.chain.key, owner: args.owner }),
427
+ proxyInitCodeHash: args.deployment.proxyInitCodeHash
428
+ });
429
+ } catch (cause) {
430
+ if (cause instanceof CustodyError) {
431
+ throw new AddressError("MALFORMED", `That deployment cannot derive an address: ${cause.message}`);
432
+ }
433
+ throw cause;
434
+ }
435
+ }
436
+ var create2Address = depositAddress;
437
+ function depositAddressBook(args) {
438
+ const book = new Map;
439
+ for (const owner of args.owners) {
440
+ const address = deriveDepositAddress({ ...args, owner });
441
+ book.set(address.toLowerCase(), owner);
442
+ }
443
+ return book;
444
+ }
445
+ var ownerOfAddress = (book, address) => book.get(address.trim().toLowerCase()) ?? null;
446
+ function assertDerivationMatches(args) {
447
+ if (args.derived.toLowerCase() !== args.reportedByChain.trim().toLowerCase()) {
448
+ throw new AddressError("MALFORMED", `Derived ${args.derived} but the factory reports ${args.reportedByChain}. ` + "The init code hash is stale — every address derived from it is unreachable.");
449
+ }
450
+ }
451
+ export {
452
+ validateAddress,
453
+ toChecksumAddress,
454
+ ownerOfAddress,
455
+ deriveDepositAddress,
456
+ depositSaltFor,
457
+ depositAddressBook,
458
+ create2Address,
459
+ assertDerivationMatches,
460
+ COUNTERFACTUAL_SCHEMES,
461
+ AddressError
462
+ };
@@ -0,0 +1,193 @@
1
+ import { type Transaction } from './ledger';
2
+ import { type Money } from './money';
3
+ import { type ChainSpec } from './chain';
4
+ /**
5
+ * Walking a chain for deposits, and crediting each exactly once.
6
+ *
7
+ * Three failures in this loop decide whether an exchange is solvent, and each
8
+ * one is silent:
9
+ *
10
+ * THE CURSOR advanced past a batch that failed halfway, and the deposits
11
+ * in the rest of that batch are never looked at again. Nobody
12
+ * notices until a customer asks.
13
+ * THE KEY a credit keyed on anything but the transfer's own identity
14
+ * double-credits the moment a range is replayed — and every
15
+ * chain integration eventually replays a range.
16
+ * THE DEPTH credited before the confirmation depth, so a reorg takes the
17
+ * transaction away and leaves the credit behind.
18
+ *
19
+ * Everything here is arranged so that getting one of them wrong is a type error
20
+ * or a failing test rather than a quiet loss.
21
+ *
22
+ * ## Why this holds no I/O
23
+ *
24
+ * `@forgezero/runtime` has no credentials and no network. A chain reader is
25
+ * injected, so the same scanner runs against a real RPC endpoint, a replay of
26
+ * archived blocks, or a fixture — and the tests exercise the actual reorg and
27
+ * retry paths rather than a mock of them.
28
+ */
29
+ export declare class DepositError extends Error {
30
+ readonly code: 'BAD_RANGE' | 'BAD_CURSOR' | 'UNKNOWN_ASSET' | 'MALFORMED';
31
+ constructor(code: 'BAD_RANGE' | 'BAD_CURSOR' | 'UNKNOWN_ASSET' | 'MALFORMED', message: string);
32
+ }
33
+ /**
34
+ * How far this chain has been walked.
35
+ *
36
+ * `scannedTo` is the last block whose deposits are FULLY accounted for. It is
37
+ * the only field that may not be advanced optimistically: everything else here
38
+ * is reporting, and this one is the record of what has been looked at.
39
+ */
40
+ export interface DepositCursor {
41
+ chain: string;
42
+ /** Last fully-processed block. Exclusive of nothing — this block is done. */
43
+ scannedTo: number;
44
+ /** Set when a pass failed, so the next one can report a stall rather than a gap. */
45
+ lastErrorAtMs?: number;
46
+ lastErrorReason?: string;
47
+ }
48
+ /** One inbound transfer, as a chain reader reports it. */
49
+ export interface ChainTransfer {
50
+ txHash: string;
51
+ /**
52
+ * Position within the transaction.
53
+ *
54
+ * Part of the identity, not decoration: one transaction can pay several
55
+ * deposit addresses, and keying on the hash alone would credit the first and
56
+ * treat the rest as duplicates of it.
57
+ */
58
+ logIndex: number;
59
+ blockNumber: number;
60
+ /** Whatever case the chain reported. Never compared directly — see below. */
61
+ to: string;
62
+ asset: string;
63
+ /** Smallest unit, as a string so no precision is lost crossing JSON. */
64
+ units: string;
65
+ }
66
+ /**
67
+ * What a chain reader must provide.
68
+ *
69
+ * `head` is separate from `transfersIn` because the depth check needs the tip
70
+ * as of the same pass. Reading it per transfer would let the tip move mid-pass
71
+ * and credit two transfers in one block at two different depths.
72
+ */
73
+ export interface ChainReader {
74
+ head(): Promise<number>;
75
+ transfersIn(range: {
76
+ fromBlock: number;
77
+ toBlock: number;
78
+ }): Promise<readonly ChainTransfer[]>;
79
+ }
80
+ /** What one pass did. Returned rather than logged, so a caller can assert on it. */
81
+ export interface ScanReport {
82
+ chain: string;
83
+ fromBlock: number;
84
+ toBlock: number;
85
+ head: number;
86
+ /** Transfers that matched a watched address. */
87
+ matched: number;
88
+ /** Credited this pass — matched, deep enough, and not already seen. */
89
+ credited: number;
90
+ /** Matched but not yet deep enough. Deliberately left for a later pass. */
91
+ pending: number;
92
+ /** Matched, deep enough, and already credited. The idempotency key working. */
93
+ duplicates: number;
94
+ /** Matched but below the chain's minimum. Not worth the gas to sweep. */
95
+ dust: number;
96
+ transactions: readonly Transaction[];
97
+ cursor: DepositCursor;
98
+ }
99
+ /**
100
+ * The identity of a transfer, and therefore of its credit.
101
+ *
102
+ * `chain:txHash:logIndex`, lowercased. Every part is load-bearing: the chain
103
+ * because two chains can produce the same hash in principle and the same hash
104
+ * string in a test for certain; the hash because it is the transfer; the log
105
+ * index because one transaction can pay several addresses.
106
+ *
107
+ * This becomes the ledger transaction's `reference`, which is also the queue's
108
+ * `dedupeKey` — so the same value that stops a double credit stops a double
109
+ * delivery, and there is nothing to keep in step.
110
+ */
111
+ export declare const depositReference: (args: {
112
+ chain: string;
113
+ txHash: string;
114
+ logIndex: number;
115
+ }) => string;
116
+ /**
117
+ * Is this transfer deep enough to believe?
118
+ *
119
+ * The depth required comes from the transfer's USD value, so a large deposit
120
+ * waits longer than a small one — a single number is too shallow at one end and
121
+ * too slow at the other, and it is always the large one that costs money.
122
+ */
123
+ export declare function isConfirmed(args: {
124
+ chain: ChainSpec;
125
+ transfer: ChainTransfer;
126
+ head: number;
127
+ usdValue: number;
128
+ }): boolean;
129
+ /**
130
+ * Turn a confirmed transfer into a balanced ledger transaction.
131
+ *
132
+ * From an `external` account to the owner's `available` bucket — so the money
133
+ * arrives from outside the system rather than appearing, and the trial balance
134
+ * still sums to zero. A deposit that credited a user without debiting anything
135
+ * would balance only by ignoring it.
136
+ */
137
+ export declare function creditDeposit(args: {
138
+ chain: string;
139
+ transfer: ChainTransfer;
140
+ owner: string;
141
+ amount: Money;
142
+ atMs: number;
143
+ }): Transaction;
144
+ /** Everything a pass needs that is not the chain itself. */
145
+ export interface ScanContext {
146
+ chain: ChainSpec;
147
+ reader: ChainReader;
148
+ /** Lowercased address → owner key. Built by `depositAddressBook`. */
149
+ addresses: Map<string, string>;
150
+ /** Already-credited references. The idempotency check. */
151
+ seen: (reference: string) => boolean | Promise<boolean>;
152
+ /** What one unit of an asset is worth, for the depth band and the dust floor. */
153
+ usdValueOf: (amount: Money) => number;
154
+ /** Below this, sweeping costs more gas than the deposit is worth. */
155
+ minDepositUsd?: number;
156
+ /** How many blocks one pass may cover. Bounded so a pass cannot run forever. */
157
+ batchSize?: number;
158
+ atMs: number;
159
+ }
160
+ /**
161
+ * One pass. Reads a bounded range, credits what is ready, advances the cursor.
162
+ *
163
+ * The cursor advances ONLY when the whole range has been accounted for. Anything
164
+ * thrown propagates with the cursor untouched, so the next pass reads the same
165
+ * range again — which is safe precisely because the reference makes a repeat
166
+ * credit a no-op. Advancing first and reconciling later is the shape that loses
167
+ * money, because the evidence of what was missed is the thing that was skipped.
168
+ */
169
+ export declare function scanOnce(cursor: DepositCursor, context: ScanContext): Promise<ScanReport>;
170
+ /**
171
+ * Replay a range without touching the cursor.
172
+ *
173
+ * Not optional. Every chain integration eventually needs to replay: a reader
174
+ * that silently returned an empty page, an asset added after the fact, an
175
+ * incident where the credits are known to be incomplete. Because the reference
176
+ * is the transfer's own identity, replaying a range that was already handled
177
+ * produces duplicates in the report and no new transactions — which is what
178
+ * makes running it safe enough to actually reach for.
179
+ */
180
+ export declare function rescanRange(range: {
181
+ fromBlock: number;
182
+ toBlock: number;
183
+ }, context: ScanContext): Promise<ScanReport>;
184
+ /**
185
+ * Where a chain's cursor starts.
186
+ *
187
+ * From the CURRENT head rather than from zero, because a new integration has no
188
+ * business replaying the chain's entire history — and `rescanRange` exists for
189
+ * the case where it does.
190
+ */
191
+ export declare const cursorFrom: (chain: string, block: number) => DepositCursor;
192
+ /** Human-readable, for a boot log or an operator screen. */
193
+ export declare const describeScan: (report: ScanReport) => string;