@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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/contracts/foundry.toml +9 -0
- package/contracts/src/ColdVault.sol +206 -0
- package/contracts/src/DepositFactory.sol +202 -0
- package/contracts/src/DepositProxy.sol +72 -0
- package/contracts/src/IERC20.sol +7 -0
- package/contracts/src/MockTokens.sol +32 -0
- package/contracts/src/SafeTransferLib.sol +31 -0
- package/contracts/test/Custody.t.sol +361 -0
- package/contracts/test/Vectors.t.sol +45 -0
- package/dist/audit.d.ts +265 -0
- package/dist/audit.js +291 -0
- package/dist/backup.d.ts +243 -0
- package/dist/backup.js +302 -0
- package/dist/calendar.d.ts +136 -0
- package/dist/calendar.js +129 -0
- package/dist/compliance.d.ts +172 -0
- package/dist/compliance.js +168 -0
- package/dist/finance/binance.d.ts +27 -0
- package/dist/finance/binance.js +452 -0
- package/dist/finance/chain-addresses.d.ts +130 -0
- package/dist/finance/chain-addresses.js +462 -0
- package/dist/finance/chain-deposits.d.ts +193 -0
- package/dist/finance/chain-deposits.js +596 -0
- package/dist/finance/chain-reconcile.d.ts +112 -0
- package/dist/finance/chain-reconcile.js +76 -0
- package/dist/finance/chain-withdrawals.d.ts +223 -0
- package/dist/finance/chain-withdrawals.js +631 -0
- package/dist/finance/chain.d.ts +116 -0
- package/dist/finance/chain.js +316 -0
- package/dist/finance/commission.d.ts +155 -0
- package/dist/finance/commission.js +419 -0
- package/dist/finance/custody.d.ts +68 -0
- package/dist/finance/custody.js +107 -0
- package/dist/finance/derive.d.ts +115 -0
- package/dist/finance/derive.js +116 -0
- package/dist/finance/discounts.d.ts +98 -0
- package/dist/finance/discounts.js +90 -0
- package/dist/finance/ledger.d.ts +221 -0
- package/dist/finance/ledger.js +308 -0
- package/dist/finance/market.d.ts +209 -0
- package/dist/finance/market.js +112 -0
- package/dist/finance/money.d.ts +118 -0
- package/dist/finance/money.js +176 -0
- package/dist/finance/rates.d.ts +178 -0
- package/dist/finance/rates.js +292 -0
- package/dist/finance/storage.d.ts +113 -0
- package/dist/finance/storage.js +226 -0
- package/dist/finance/tax.d.ts +132 -0
- package/dist/finance/tax.js +291 -0
- package/dist/finance/transfers.d.ts +153 -0
- package/dist/finance/transfers.js +292 -0
- package/dist/finance/venues.d.ts +190 -0
- package/dist/finance/venues.js +251 -0
- package/dist/identity.d.ts +115 -0
- package/dist/identity.js +111 -0
- package/dist/importers.d.ts +87 -0
- package/dist/importers.js +250 -0
- package/dist/jobs.d.ts +171 -0
- package/dist/jobs.js +250 -0
- package/dist/notify-templates.d.ts +11 -0
- package/dist/notify-templates.js +254 -0
- package/dist/notify.d.ts +172 -0
- package/dist/notify.js +122 -0
- package/dist/openssh.d.ts +36 -0
- package/dist/openssh.js +106 -0
- package/dist/otpauth.d.ts +57 -0
- package/dist/otpauth.js +223 -0
- package/dist/outbox.d.ts +234 -0
- package/dist/outbox.js +236 -0
- package/dist/passkey.d.ts +120 -0
- package/dist/passkey.js +105 -0
- package/dist/phrase.d.ts +87 -0
- package/dist/phrase.js +87 -0
- package/dist/pipeline.d.ts +137 -0
- package/dist/pipeline.js +121 -0
- package/dist/queue.d.ts +243 -0
- package/dist/queue.js +246 -0
- package/dist/schema-typebox.d.ts +24 -0
- package/dist/schema-typebox.js +201 -0
- package/dist/schema.d.ts +134 -0
- package/dist/schema.js +169 -0
- package/dist/serial.d.ts +54 -0
- package/dist/serial.js +40 -0
- package/dist/slip10.d.ts +37 -0
- package/dist/slip10.js +74 -0
- package/dist/snp.d.ts +115 -0
- package/dist/snp.js +109 -0
- package/dist/ssh-agent.d.ts +70 -0
- package/dist/ssh-agent.js +141 -0
- package/dist/ssh-cert.d.ts +73 -0
- package/dist/ssh-cert.js +111 -0
- package/dist/totp.d.ts +104 -0
- package/dist/totp.js +143 -0
- package/package.json +248 -0
|
@@ -0,0 +1,631 @@
|
|
|
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-withdrawals.ts
|
|
442
|
+
class WithdrawalError extends Error {
|
|
443
|
+
code;
|
|
444
|
+
constructor(code, message) {
|
|
445
|
+
super(message);
|
|
446
|
+
this.code = code;
|
|
447
|
+
this.name = "WithdrawalError";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
var WITHDRAWAL_STATES = [
|
|
451
|
+
"requested",
|
|
452
|
+
"approved",
|
|
453
|
+
"dispatched",
|
|
454
|
+
"confirmed",
|
|
455
|
+
"rejected",
|
|
456
|
+
"failed",
|
|
457
|
+
"refunded"
|
|
458
|
+
];
|
|
459
|
+
var TRANSITIONS = {
|
|
460
|
+
requested: ["approved", "rejected"],
|
|
461
|
+
approved: ["dispatched", "rejected"],
|
|
462
|
+
dispatched: ["confirmed", "failed"],
|
|
463
|
+
confirmed: [],
|
|
464
|
+
rejected: [],
|
|
465
|
+
failed: ["refunded"],
|
|
466
|
+
refunded: []
|
|
467
|
+
};
|
|
468
|
+
var canTransition = (from, to) => TRANSITIONS[from].includes(to);
|
|
469
|
+
function assertTransition(withdrawal, to) {
|
|
470
|
+
if (!canTransition(withdrawal.state, to)) {
|
|
471
|
+
throw new WithdrawalError("BAD_STATE", `A withdrawal cannot go from ${withdrawal.state} to ${to}. ` + `From ${withdrawal.state} the only moves are: ${TRANSITIONS[withdrawal.state].join(", ") || "none"}.`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
var holdReference = (key) => `withdrawal:hold:${key}`;
|
|
475
|
+
var broadcastKeyFor = (withdrawal) => `withdrawal:send:${withdrawal.chain}:${withdrawal.key}`;
|
|
476
|
+
function requestWithdrawal(args) {
|
|
477
|
+
if (!args.key?.trim())
|
|
478
|
+
throw new WithdrawalError("MALFORMED", "A withdrawal needs a key.");
|
|
479
|
+
if (!args.chain.enabled) {
|
|
480
|
+
throw new WithdrawalError("CHAIN_DISABLED", `${args.chain.label} is not processing withdrawals.`);
|
|
481
|
+
}
|
|
482
|
+
if (args.amount.units <= 0n) {
|
|
483
|
+
throw new WithdrawalError("BAD_AMOUNT", "A withdrawal is for a positive amount.");
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
validateAddress(args.destination, args.chain);
|
|
487
|
+
} catch (cause) {
|
|
488
|
+
throw new WithdrawalError("BAD_ADDRESS", `That is not a ${args.chain.label} address. ${cause instanceof Error ? cause.message : ""}`.trim());
|
|
489
|
+
}
|
|
490
|
+
if (args.available.asset !== args.amount.asset) {
|
|
491
|
+
throw new WithdrawalError("MALFORMED", "The available balance is in a different asset.");
|
|
492
|
+
}
|
|
493
|
+
if (args.available.units < args.amount.units) {
|
|
494
|
+
throw new WithdrawalError("INSUFFICIENT", "That is more than the available balance.");
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
withdrawal: {
|
|
498
|
+
key: args.key,
|
|
499
|
+
owner: args.owner,
|
|
500
|
+
chain: args.chain.key,
|
|
501
|
+
destination: args.destination,
|
|
502
|
+
amount: args.amount,
|
|
503
|
+
state: "requested",
|
|
504
|
+
requestedAtMs: args.atMs
|
|
505
|
+
},
|
|
506
|
+
hold: placeHold({
|
|
507
|
+
reference: holdReference(args.key),
|
|
508
|
+
owner: args.owner,
|
|
509
|
+
amount: args.amount,
|
|
510
|
+
atMs: args.atMs
|
|
511
|
+
})
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function approveWithdrawal(withdrawal, args) {
|
|
515
|
+
assertTransition(withdrawal, "approved");
|
|
516
|
+
if (!args.actorUserKey?.trim()) {
|
|
517
|
+
throw new WithdrawalError("MALFORMED", "An approval is always attributed.");
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
...withdrawal,
|
|
521
|
+
state: "approved",
|
|
522
|
+
decidedByUserKey: args.actorUserKey,
|
|
523
|
+
broadcastKey: broadcastKeyFor(withdrawal)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function rejectWithdrawal(withdrawal, args) {
|
|
527
|
+
assertTransition(withdrawal, "rejected");
|
|
528
|
+
if (!args.reason?.trim()) {
|
|
529
|
+
throw new WithdrawalError("MALFORMED", "A rejection carries a reason — the customer is told it.");
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
withdrawal: {
|
|
533
|
+
...withdrawal,
|
|
534
|
+
state: "rejected",
|
|
535
|
+
decidedByUserKey: args.actorUserKey,
|
|
536
|
+
reason: args.reason
|
|
537
|
+
},
|
|
538
|
+
release: releaseHold({
|
|
539
|
+
reference: `withdrawal:release:${withdrawal.key}`,
|
|
540
|
+
owner: withdrawal.owner,
|
|
541
|
+
amount: withdrawal.amount,
|
|
542
|
+
atMs: args.atMs
|
|
543
|
+
})
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
async function processOnce(withdrawal, args) {
|
|
547
|
+
assertTransition(withdrawal, "dispatched");
|
|
548
|
+
const broadcastKey = withdrawal.broadcastKey ?? broadcastKeyFor(withdrawal);
|
|
549
|
+
const sent = await args.broadcaster.send({
|
|
550
|
+
broadcastKey,
|
|
551
|
+
chain: withdrawal.chain,
|
|
552
|
+
destination: withdrawal.destination,
|
|
553
|
+
amount: withdrawal.amount
|
|
554
|
+
});
|
|
555
|
+
return { ...withdrawal, state: "dispatched", broadcastKey, txHash: sent.txHash };
|
|
556
|
+
}
|
|
557
|
+
function confirmWithdrawal(withdrawal, args) {
|
|
558
|
+
assertTransition(withdrawal, "confirmed");
|
|
559
|
+
return {
|
|
560
|
+
withdrawal: { ...withdrawal, state: "confirmed", txHash: args.txHash },
|
|
561
|
+
capture: captureHold({
|
|
562
|
+
reference: `withdrawal:capture:${withdrawal.key}`,
|
|
563
|
+
owner: withdrawal.owner,
|
|
564
|
+
to: { kind: "external", owner: withdrawal.chain, bucket: "available" },
|
|
565
|
+
amount: withdrawal.amount,
|
|
566
|
+
atMs: args.atMs
|
|
567
|
+
})
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function markFailed(withdrawal, args) {
|
|
571
|
+
assertTransition(withdrawal, "failed");
|
|
572
|
+
return { ...withdrawal, state: "failed", reason: args.reason };
|
|
573
|
+
}
|
|
574
|
+
function refundFailed(withdrawal, args) {
|
|
575
|
+
assertTransition(withdrawal, "refunded");
|
|
576
|
+
return {
|
|
577
|
+
withdrawal: { ...withdrawal, state: "refunded" },
|
|
578
|
+
release: releaseHold({
|
|
579
|
+
reference: `withdrawal:refund:${withdrawal.key}`,
|
|
580
|
+
owner: withdrawal.owner,
|
|
581
|
+
amount: withdrawal.amount,
|
|
582
|
+
atMs: args.atMs
|
|
583
|
+
})
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function sweepForPayout(args) {
|
|
587
|
+
const asset = args.need.asset;
|
|
588
|
+
if (args.hot.asset !== asset) {
|
|
589
|
+
throw new WithdrawalError("MALFORMED", "The hot balance is in a different asset.");
|
|
590
|
+
}
|
|
591
|
+
const sweep = [];
|
|
592
|
+
const uneconomic = [];
|
|
593
|
+
let raised = args.hot.units;
|
|
594
|
+
const ordered = [...args.candidates].sort((a, b) => b.amount.units > a.amount.units ? 1 : -1);
|
|
595
|
+
for (const candidate of ordered) {
|
|
596
|
+
if (candidate.amount.asset !== asset)
|
|
597
|
+
continue;
|
|
598
|
+
const net = candidate.amount.units - candidate.estimatedGas.units;
|
|
599
|
+
if (net <= 0n) {
|
|
600
|
+
uneconomic.push(candidate);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (raised >= args.need.units)
|
|
604
|
+
break;
|
|
605
|
+
sweep.push(candidate);
|
|
606
|
+
raised += net;
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
sweep,
|
|
610
|
+
uneconomic,
|
|
611
|
+
raised: { units: raised, asset },
|
|
612
|
+
short: raised < args.need.units
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
var describeSweep = (plan) => `${plan.sweep.length} address(es) to sweep, ${plan.uneconomic.length} not worth the gas` + (plan.short ? " — still short of the payout" : "");
|
|
616
|
+
export {
|
|
617
|
+
sweepForPayout,
|
|
618
|
+
requestWithdrawal,
|
|
619
|
+
rejectWithdrawal,
|
|
620
|
+
refundFailed,
|
|
621
|
+
processOnce,
|
|
622
|
+
markFailed,
|
|
623
|
+
holdReference,
|
|
624
|
+
describeSweep,
|
|
625
|
+
confirmWithdrawal,
|
|
626
|
+
canTransition,
|
|
627
|
+
broadcastKeyFor,
|
|
628
|
+
approveWithdrawal,
|
|
629
|
+
WithdrawalError,
|
|
630
|
+
WITHDRAWAL_STATES
|
|
631
|
+
};
|