@forgezero/runtime 0.1.14 → 0.1.15
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/README.md +27 -388
- package/dist/notify-templates.js +1 -1
- package/dist/schema-typebox.js +116 -7
- package/dist/schema.d.ts +14 -1
- package/dist/schema.js +116 -7
- package/package.json +2 -58
- package/dist/compliance.d.ts +0 -172
- package/dist/compliance.js +0 -168
- package/dist/finance/chain-addresses.d.ts +0 -130
- package/dist/finance/chain-addresses.js +0 -462
- package/dist/finance/chain-deposits.d.ts +0 -193
- package/dist/finance/chain-deposits.js +0 -600
- package/dist/finance/chain-reconcile.d.ts +0 -112
- package/dist/finance/chain-reconcile.js +0 -76
- package/dist/finance/chain-withdrawals.d.ts +0 -223
- package/dist/finance/chain-withdrawals.js +0 -635
- package/dist/finance/chain.d.ts +0 -116
- package/dist/finance/chain.js +0 -316
- package/dist/finance/commission.d.ts +0 -155
- package/dist/finance/commission.js +0 -423
- package/dist/finance/custody.d.ts +0 -68
- package/dist/finance/custody.js +0 -107
- package/dist/finance/derive.d.ts +0 -115
- package/dist/finance/derive.js +0 -116
- package/dist/finance/ledger.d.ts +0 -227
- package/dist/finance/ledger.js +0 -313
- package/dist/finance/market.d.ts +0 -209
- package/dist/finance/market.js +0 -112
- package/dist/finance/rates.d.ts +0 -178
- package/dist/finance/rates.js +0 -292
- package/dist/finance/transfers.d.ts +0 -153
- package/dist/finance/transfers.js +0 -292
- package/dist/finance/venues.d.ts +0 -190
- package/dist/finance/venues.js +0 -251
|
@@ -1,423 +0,0 @@
|
|
|
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
|
-
var MAX_LEDGER_ENTRIES = 64;
|
|
172
|
-
function assertBalanced(transaction) {
|
|
173
|
-
if (transaction.entries.length < 2) {
|
|
174
|
-
throw new LedgerError("EMPTY_TRANSACTION", "A transaction needs at least two entries.");
|
|
175
|
-
}
|
|
176
|
-
if (transaction.entries.length > MAX_LEDGER_ENTRIES) {
|
|
177
|
-
throw new LedgerError("TOO_MANY_ENTRIES", `A transaction may contain at most ${MAX_LEDGER_ENTRIES} entries; split this batch into separate transactions.`);
|
|
178
|
-
}
|
|
179
|
-
const totals = new Map;
|
|
180
|
-
for (const entry of transaction.entries) {
|
|
181
|
-
if (entry.amount.units === 0n) {
|
|
182
|
-
throw new LedgerError("ZERO_ENTRY", `An entry on ${accountId(entry.account)} moves nothing. Remove it or give it an amount.`);
|
|
183
|
-
}
|
|
184
|
-
totals.set(entry.amount.asset, (totals.get(entry.amount.asset) ?? 0n) + entry.amount.units);
|
|
185
|
-
}
|
|
186
|
-
for (const [asset, total] of totals) {
|
|
187
|
-
if (total !== 0n) {
|
|
188
|
-
throw new LedgerError("UNBALANCED", `${asset} does not sum to zero — it is off by ${total} minor units. Every movement needs a counterparty.`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function transfer(args) {
|
|
193
|
-
const transaction = {
|
|
194
|
-
reference: args.reference,
|
|
195
|
-
kind: args.kind,
|
|
196
|
-
atMs: args.atMs,
|
|
197
|
-
memo: args.memo,
|
|
198
|
-
entries: [
|
|
199
|
-
{ account: args.from, amount: { units: -args.amount.units, asset: args.amount.asset } },
|
|
200
|
-
{ account: args.to, amount: args.amount }
|
|
201
|
-
]
|
|
202
|
-
};
|
|
203
|
-
assertBalanced(transaction);
|
|
204
|
-
return transaction;
|
|
205
|
-
}
|
|
206
|
-
var placeHold = (args) => transfer({
|
|
207
|
-
reference: args.reference,
|
|
208
|
-
kind: "hold.place",
|
|
209
|
-
from: { kind: args.kind ?? "user", owner: args.owner, bucket: "available" },
|
|
210
|
-
to: { kind: args.kind ?? "user", owner: args.owner, bucket: "held" },
|
|
211
|
-
amount: args.amount,
|
|
212
|
-
atMs: args.atMs,
|
|
213
|
-
memo: args.memo
|
|
214
|
-
});
|
|
215
|
-
var releaseHold = (args) => transfer({
|
|
216
|
-
reference: args.reference,
|
|
217
|
-
kind: "hold.release",
|
|
218
|
-
from: { kind: args.kind ?? "user", owner: args.owner, bucket: "held" },
|
|
219
|
-
to: { kind: args.kind ?? "user", owner: args.owner, bucket: "available" },
|
|
220
|
-
amount: args.amount,
|
|
221
|
-
atMs: args.atMs
|
|
222
|
-
});
|
|
223
|
-
var captureHold = (args) => transfer({
|
|
224
|
-
reference: args.reference,
|
|
225
|
-
kind: "hold.capture",
|
|
226
|
-
from: { kind: args.kind ?? "user", owner: args.owner, bucket: "held" },
|
|
227
|
-
to: args.to,
|
|
228
|
-
amount: args.amount,
|
|
229
|
-
atMs: args.atMs,
|
|
230
|
-
memo: args.memo
|
|
231
|
-
});
|
|
232
|
-
function balancesFrom(transactions) {
|
|
233
|
-
const balances = new Map;
|
|
234
|
-
for (const transaction of transactions) {
|
|
235
|
-
for (const entry of transaction.entries) {
|
|
236
|
-
const id = accountId(entry.account);
|
|
237
|
-
const perAsset = balances.get(id) ?? new Map;
|
|
238
|
-
const current = perAsset.get(entry.amount.asset) ?? zero(entry.amount.asset);
|
|
239
|
-
perAsset.set(entry.amount.asset, add(current, entry.amount));
|
|
240
|
-
balances.set(id, perAsset);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return balances;
|
|
244
|
-
}
|
|
245
|
-
function balanceOf(balances, account, asset) {
|
|
246
|
-
return balances.get(accountId(account))?.get(asset) ?? zero(asset);
|
|
247
|
-
}
|
|
248
|
-
var availableOf = (balances, owner, asset, kind = "user") => balanceOf(balances, { kind, owner, bucket: "available" }, asset);
|
|
249
|
-
var heldOf = (balances, owner, asset, kind = "user") => balanceOf(balances, { kind, owner, bucket: "held" }, asset);
|
|
250
|
-
var totalOf = (balances, owner, asset, kind = "user") => add(availableOf(balances, owner, asset, kind), heldOf(balances, owner, asset, kind));
|
|
251
|
-
function trialBalance(transactions) {
|
|
252
|
-
const totals = new Map;
|
|
253
|
-
const accounts = new Map;
|
|
254
|
-
for (const transaction of transactions) {
|
|
255
|
-
for (const entry of transaction.entries) {
|
|
256
|
-
totals.set(entry.amount.asset, (totals.get(entry.amount.asset) ?? 0n) + entry.amount.units);
|
|
257
|
-
const seen = accounts.get(entry.amount.asset) ?? new Set;
|
|
258
|
-
seen.add(accountId(entry.account));
|
|
259
|
-
accounts.set(entry.amount.asset, seen);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const perAsset = [...totals.entries()].map(([asset, total]) => ({
|
|
263
|
-
asset,
|
|
264
|
-
total,
|
|
265
|
-
accounts: accounts.get(asset)?.size ?? 0
|
|
266
|
-
}));
|
|
267
|
-
const discrepancies = perAsset.filter((row) => row.total !== 0n).map((row) => ({ asset: row.asset, off: row.total }));
|
|
268
|
-
return { ok: discrepancies.length === 0, perAsset, discrepancies };
|
|
269
|
-
}
|
|
270
|
-
function statement(transactions, account, asset) {
|
|
271
|
-
const id = accountId(account);
|
|
272
|
-
let running = zero(asset);
|
|
273
|
-
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) => {
|
|
274
|
-
running = add(running, entry.amount);
|
|
275
|
-
return {
|
|
276
|
-
atMs: transaction.atMs,
|
|
277
|
-
reference: transaction.reference,
|
|
278
|
-
kind: transaction.kind,
|
|
279
|
-
amount: formatAmount(entry.amount, { trim: true }),
|
|
280
|
-
balance: formatAmount(running, { trim: true })
|
|
281
|
-
};
|
|
282
|
-
}));
|
|
283
|
-
}
|
|
284
|
-
function assertAvailable(balances, args) {
|
|
285
|
-
const available = availableOf(balances, args.owner, args.amount.asset, args.kind ?? "user");
|
|
286
|
-
if (subtract(available, args.amount).units < 0n) {
|
|
287
|
-
throw new LedgerError("INSUFFICIENT_AVAILABLE", `${args.owner} has ${formatAmount(available, { trim: true })} ${args.amount.asset} available; this needs ${formatAmount(args.amount, { trim: true })}.`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
var VERSION2 = "0.1.0";
|
|
291
|
-
|
|
292
|
-
// src/finance/commission.ts
|
|
293
|
-
class CommissionError extends Error {
|
|
294
|
-
code;
|
|
295
|
-
constructor(code, message) {
|
|
296
|
-
super(message);
|
|
297
|
-
this.code = code;
|
|
298
|
-
this.name = "CommissionError";
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
var RATE = /^\d+(\.\d+)?$/;
|
|
302
|
-
function assertRate(rate, field) {
|
|
303
|
-
if (typeof rate !== "string") {
|
|
304
|
-
throw new CommissionError("BAD_RATE", `${field} must be a decimal STRING, not a ${typeof rate}. A rate as a float literal is how 0.3 becomes 0.30000000000000004.`);
|
|
305
|
-
}
|
|
306
|
-
if (!RATE.test(rate.trim()) || Number(rate) > 1) {
|
|
307
|
-
throw new CommissionError("BAD_RATE", `${field} must be a decimal string between 0 and 1, not "${rate}". A rate as a float literal is how 0.3 becomes 0.30000000000000004.`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
function periodProfit(performance) {
|
|
311
|
-
const asset = performance.openingEquity.asset;
|
|
312
|
-
for (const [name, amount] of Object.entries(performance)) {
|
|
313
|
-
if (amount.asset !== asset) {
|
|
314
|
-
throw new CommissionError("ASSET_MISMATCH", `${name} is in ${amount.asset} and the period opens in ${asset}.`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
const gross = subtract(performance.closingEquity, performance.openingEquity);
|
|
318
|
-
return add(subtract(gross, performance.deposits), performance.withdrawals);
|
|
319
|
-
}
|
|
320
|
-
function chargeableProfit(performance) {
|
|
321
|
-
const asset = performance.openingEquity.asset;
|
|
322
|
-
const profit = periodProfit(performance);
|
|
323
|
-
if (profit.units <= 0n)
|
|
324
|
-
return zero(asset);
|
|
325
|
-
const netClosing = add(subtract(performance.closingEquity, performance.deposits), performance.withdrawals);
|
|
326
|
-
if (netClosing.units <= performance.highWaterMark.units)
|
|
327
|
-
return zero(asset);
|
|
328
|
-
const aboveMark = subtract(netClosing, performance.highWaterMark);
|
|
329
|
-
return aboveMark.units < profit.units ? aboveMark : profit;
|
|
330
|
-
}
|
|
331
|
-
var nextHighWaterMark = (performance) => {
|
|
332
|
-
const netClosing = add(subtract(performance.closingEquity, performance.deposits), performance.withdrawals);
|
|
333
|
-
return netClosing.units > performance.highWaterMark.units ? netClosing : performance.highWaterMark;
|
|
334
|
-
};
|
|
335
|
-
function splitCommission(input) {
|
|
336
|
-
assertRate(input.commissionRate, "The commission rate");
|
|
337
|
-
if (input.referralRate)
|
|
338
|
-
assertRate(input.referralRate, "The referral rate");
|
|
339
|
-
const asset = input.profit.asset;
|
|
340
|
-
if (input.profit.units <= 0n) {
|
|
341
|
-
return { total: zero(asset), platform: zero(asset), referral: zero(asset) };
|
|
342
|
-
}
|
|
343
|
-
const total = mulRate(input.profit, input.commissionRate, "down");
|
|
344
|
-
if (!input.referrer || !input.referralRate || total.units === 0n) {
|
|
345
|
-
return { total, platform: total, referral: zero(asset) };
|
|
346
|
-
}
|
|
347
|
-
const referral = mulRate(total, input.referralRate, "down");
|
|
348
|
-
return {
|
|
349
|
-
total,
|
|
350
|
-
platform: subtract(total, referral),
|
|
351
|
-
referral,
|
|
352
|
-
referrer: input.referrer
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
function commissionTransaction(args) {
|
|
356
|
-
if (args.split.total.units === 0n)
|
|
357
|
-
return null;
|
|
358
|
-
const revenue = args.revenueAccount ?? { kind: "platform", owner: "revenue" };
|
|
359
|
-
const from = { kind: "user", owner: args.owner, bucket: "available" };
|
|
360
|
-
if (args.split.referral.units === 0n) {
|
|
361
|
-
return transfer({
|
|
362
|
-
reference: args.reference,
|
|
363
|
-
kind: "commission",
|
|
364
|
-
from,
|
|
365
|
-
to: revenue,
|
|
366
|
-
amount: args.split.total,
|
|
367
|
-
atMs: args.atMs
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
return {
|
|
371
|
-
reference: args.reference,
|
|
372
|
-
kind: "commission",
|
|
373
|
-
atMs: args.atMs,
|
|
374
|
-
memo: `commission with referral to ${args.split.referrer}`,
|
|
375
|
-
entries: [
|
|
376
|
-
{ account: from, amount: { units: -args.split.total.units, asset: args.split.total.asset } },
|
|
377
|
-
{ account: revenue, amount: args.split.platform },
|
|
378
|
-
{
|
|
379
|
-
account: { kind: "user", owner: args.split.referrer, bucket: "available" },
|
|
380
|
-
amount: args.split.referral
|
|
381
|
-
}
|
|
382
|
-
]
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
function projectPrepay(input) {
|
|
386
|
-
assertRate(input.projectedReturnRate, "The projected return");
|
|
387
|
-
assertRate(input.commissionRate, "The commission rate");
|
|
388
|
-
assertRate(input.prepayRate, "The prepay rate");
|
|
389
|
-
const projectedProfit = mulRate(input.subscriptionAmount, input.projectedReturnRate, "down");
|
|
390
|
-
const projectedCommission = mulRate(projectedProfit, input.commissionRate, "down");
|
|
391
|
-
return {
|
|
392
|
-
projectedProfit,
|
|
393
|
-
projectedCommission,
|
|
394
|
-
due: mulRate(projectedCommission, input.prepayRate, "down")
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
function splitReferrals(income, shares) {
|
|
398
|
-
let remaining = income;
|
|
399
|
-
const paid = [];
|
|
400
|
-
for (const share of shares) {
|
|
401
|
-
assertRate(share.rate, `The rate for ${share.referrer}`);
|
|
402
|
-
const amount = mulRate(income, share.rate, "down");
|
|
403
|
-
if (amount.units === 0n)
|
|
404
|
-
continue;
|
|
405
|
-
if (amount.units > remaining.units) {
|
|
406
|
-
throw new CommissionError("BAD_RATE", `Referral rates total more than the income of ${formatAmount(income, { trim: true })} ${income.asset}.`);
|
|
407
|
-
}
|
|
408
|
-
remaining = subtract(remaining, amount);
|
|
409
|
-
paid.push({ referrer: share.referrer, amount });
|
|
410
|
-
}
|
|
411
|
-
return { platform: remaining, paid };
|
|
412
|
-
}
|
|
413
|
-
export {
|
|
414
|
-
splitReferrals,
|
|
415
|
-
splitCommission,
|
|
416
|
-
projectPrepay,
|
|
417
|
-
periodProfit,
|
|
418
|
-
nextHighWaterMark,
|
|
419
|
-
commissionTransaction,
|
|
420
|
-
chargeableProfit,
|
|
421
|
-
allocate,
|
|
422
|
-
CommissionError
|
|
423
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export declare class CustodyError extends Error {
|
|
2
|
-
readonly code: 'MALFORMED' | 'NOT_AN_ADDRESS' | 'BAD_THRESHOLD';
|
|
3
|
-
constructor(code: 'MALFORMED' | 'NOT_AN_ADDRESS' | 'BAD_THRESHOLD', message: string);
|
|
4
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* EIP-55 checksum.
|
|
7
|
-
*
|
|
8
|
-
* A lowercase address is valid and checks nothing. The mixed-case form catches
|
|
9
|
-
* a mistyped character, which for a treasury destination is the difference
|
|
10
|
-
* between a failed transaction and an unrecoverable one.
|
|
11
|
-
*/
|
|
12
|
-
export declare function toChecksumAddress(address: string): string;
|
|
13
|
-
/**
|
|
14
|
-
* A salt from a namespace and a key.
|
|
15
|
-
*
|
|
16
|
-
* The contract treats a salt as opaque, so the CONVENTION is the platform's.
|
|
17
|
-
* This only makes it consistent: a namespace stops a user key and an invoice
|
|
18
|
-
* key that happen to share a value from deriving the same address, which would
|
|
19
|
-
* silently merge two balances.
|
|
20
|
-
*/
|
|
21
|
-
export declare function saltFor(namespace: string, key: string): string;
|
|
22
|
-
/**
|
|
23
|
-
* The CREATE2 deposit address for a salt.
|
|
24
|
-
*
|
|
25
|
-
* `keccak256(0xff ++ factory ++ salt ++ initCodeHash)`, last 20 bytes. The
|
|
26
|
-
* init-code hash comes from the deployed factory's `PROXY_INIT_CODE_HASH`
|
|
27
|
-
* rather than being hardcoded: a compiler change alters it, and a stale
|
|
28
|
-
* constant would derive addresses the factory cannot sweep.
|
|
29
|
-
*/
|
|
30
|
-
export declare function depositAddress(args: {
|
|
31
|
-
factory: string;
|
|
32
|
-
salt: string;
|
|
33
|
-
proxyInitCodeHash: string;
|
|
34
|
-
}): string;
|
|
35
|
-
export declare function domainSeparator(args: {
|
|
36
|
-
chainId: bigint;
|
|
37
|
-
vault: string;
|
|
38
|
-
}): string;
|
|
39
|
-
/**
|
|
40
|
-
* What each custodian signs, computed offline.
|
|
41
|
-
*
|
|
42
|
-
* Four things are inside it and each closes a replay: `chainId` stops a testnet
|
|
43
|
-
* rehearsal executing on mainnet, `vault` stops approvals for one deployment
|
|
44
|
-
* being pointed at another, `nonce` makes an approval spendable once, and the
|
|
45
|
-
* request itself means changing the amount or destination voids every signature
|
|
46
|
-
* already collected.
|
|
47
|
-
*/
|
|
48
|
-
export declare function withdrawDigest(args: {
|
|
49
|
-
chainId: bigint;
|
|
50
|
-
vault: string;
|
|
51
|
-
token: string;
|
|
52
|
-
to: string;
|
|
53
|
-
amount: bigint;
|
|
54
|
-
nonce: bigint;
|
|
55
|
-
}): string;
|
|
56
|
-
/**
|
|
57
|
-
* Order signatures the way the vault requires: ascending by signer address.
|
|
58
|
-
*
|
|
59
|
-
* Not cosmetic. The contract uses strict ordering to reject the same custodian
|
|
60
|
-
* signing twice in one submission, so an unsorted batch of otherwise valid
|
|
61
|
-
* approvals is refused — and a batch sorted the wrong way makes an M-of-N vault
|
|
62
|
-
* satisfiable by one custodian.
|
|
63
|
-
*/
|
|
64
|
-
export declare function orderSignatures(signatures: readonly {
|
|
65
|
-
signer: string;
|
|
66
|
-
signature: string;
|
|
67
|
-
}[]): string[];
|
|
68
|
-
export declare const VERSION = "0.1.0";
|
package/dist/finance/custody.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
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/custody.ts
|
|
10
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
11
|
-
var hex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12
|
-
var bytes = (value) => {
|
|
13
|
-
const bare = value.replace(/^0x/i, "");
|
|
14
|
-
if (bare.length % 2)
|
|
15
|
-
throw new CustodyError("MALFORMED", `Not byte-aligned: ${value}`);
|
|
16
|
-
const out = new Uint8Array(bare.length / 2);
|
|
17
|
-
for (let i = 0;i < out.length; i++)
|
|
18
|
-
out[i] = parseInt(bare.slice(i * 2, i * 2 + 2), 16);
|
|
19
|
-
return out;
|
|
20
|
-
};
|
|
21
|
-
var concat = (...parts) => {
|
|
22
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
23
|
-
let at = 0;
|
|
24
|
-
for (const part of parts) {
|
|
25
|
-
out.set(part, at);
|
|
26
|
-
at += part.length;
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
class CustodyError extends Error {
|
|
32
|
-
code;
|
|
33
|
-
constructor(code, message) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.name = "CustodyError";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
var assertAddress = (value, what) => {
|
|
40
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
|
|
41
|
-
throw new CustodyError("NOT_AN_ADDRESS", `${what} is not an address: ${value}`);
|
|
42
|
-
}
|
|
43
|
-
return value;
|
|
44
|
-
};
|
|
45
|
-
function toChecksumAddress(address) {
|
|
46
|
-
const bare = assertAddress(address, "address").slice(2).toLowerCase();
|
|
47
|
-
const digest = hex(keccak_256(new TextEncoder().encode(bare)));
|
|
48
|
-
let out = "0x";
|
|
49
|
-
for (let i = 0;i < bare.length; i++) {
|
|
50
|
-
out += parseInt(digest[i], 16) >= 8 ? bare[i].toUpperCase() : bare[i];
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
function saltFor(namespace, key) {
|
|
55
|
-
return `0x${hex(keccak_256(new TextEncoder().encode(`${namespace}:${key}`)))}`;
|
|
56
|
-
}
|
|
57
|
-
function depositAddress(args) {
|
|
58
|
-
assertAddress(args.factory, "factory");
|
|
59
|
-
const salt = bytes(args.salt);
|
|
60
|
-
if (salt.length !== 32)
|
|
61
|
-
throw new CustodyError("MALFORMED", "A salt is 32 bytes.");
|
|
62
|
-
const initCodeHash = bytes(args.proxyInitCodeHash);
|
|
63
|
-
if (initCodeHash.length !== 32)
|
|
64
|
-
throw new CustodyError("MALFORMED", "An init code hash is 32 bytes.");
|
|
65
|
-
const digest = keccak_256(concat(Uint8Array.of(255), bytes(args.factory), salt, initCodeHash));
|
|
66
|
-
return toChecksumAddress(`0x${hex(digest.slice(12))}`);
|
|
67
|
-
}
|
|
68
|
-
var keccakText = (value) => keccak_256(new TextEncoder().encode(value));
|
|
69
|
-
var word = (value) => {
|
|
70
|
-
const raw = typeof value === "bigint" ? value.toString(16) : value.replace(/^0x/i, "");
|
|
71
|
-
return bytes(raw.padStart(64, "0"));
|
|
72
|
-
};
|
|
73
|
-
var DOMAIN_TYPEHASH = keccakText("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
|
74
|
-
var WITHDRAW_TYPEHASH = keccakText("Withdraw(address token,address to,uint256 amount,uint256 nonce)");
|
|
75
|
-
function domainSeparator(args) {
|
|
76
|
-
assertAddress(args.vault, "vault");
|
|
77
|
-
return `0x${hex(keccak_256(concat(DOMAIN_TYPEHASH, keccakText("ForgeZeroColdVault"), keccakText("1"), word(args.chainId), word(args.vault))))}`;
|
|
78
|
-
}
|
|
79
|
-
function withdrawDigest(args) {
|
|
80
|
-
assertAddress(args.to, "to");
|
|
81
|
-
assertAddress(args.token, "token");
|
|
82
|
-
const structHash = keccak_256(concat(WITHDRAW_TYPEHASH, word(args.token), word(args.to), word(args.amount), word(args.nonce)));
|
|
83
|
-
const separator = bytes(domainSeparator({ chainId: args.chainId, vault: args.vault }));
|
|
84
|
-
return `0x${hex(keccak_256(concat(Uint8Array.of(25, 1), separator, structHash)))}`;
|
|
85
|
-
}
|
|
86
|
-
function orderSignatures(signatures) {
|
|
87
|
-
const seen = new Set;
|
|
88
|
-
for (const entry of signatures) {
|
|
89
|
-
const key = assertAddress(entry.signer, "signer").toLowerCase();
|
|
90
|
-
if (seen.has(key)) {
|
|
91
|
-
throw new CustodyError("BAD_THRESHOLD", `${entry.signer} signed twice; that is one approval.`);
|
|
92
|
-
}
|
|
93
|
-
seen.add(key);
|
|
94
|
-
}
|
|
95
|
-
return [...signatures].sort((a, b) => BigInt(a.signer) < BigInt(b.signer) ? -1 : 1).map((entry) => entry.signature);
|
|
96
|
-
}
|
|
97
|
-
var VERSION = "0.1.0";
|
|
98
|
-
export {
|
|
99
|
-
withdrawDigest,
|
|
100
|
-
toChecksumAddress,
|
|
101
|
-
saltFor,
|
|
102
|
-
orderSignatures,
|
|
103
|
-
domainSeparator,
|
|
104
|
-
depositAddress,
|
|
105
|
-
VERSION,
|
|
106
|
-
CustodyError
|
|
107
|
-
};
|