@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which chains exist, and what it takes to believe a transfer on one.
|
|
3
|
+
*
|
|
4
|
+
* Data an admin edits, never a constant. The prior art here got it wrong in a
|
|
5
|
+
* specific and expensive way: two BSC entries hard-coded in a TypeScript
|
|
6
|
+
* constant, so listing a token was a deploy. Two exchanges will not share one
|
|
7
|
+
* chain list and neither will wait for a release to add an asset.
|
|
8
|
+
*
|
|
9
|
+
* ## Confirmations are the whole point
|
|
10
|
+
*
|
|
11
|
+
* Every other field is bookkeeping. `confirmations` is the number that decides
|
|
12
|
+
* whether the platform pays out on a transaction that later disappears in a
|
|
13
|
+
* reorg — credit too early and a deep reorg is a real loss, credit too late and
|
|
14
|
+
* deposits take an hour nobody understands.
|
|
15
|
+
*
|
|
16
|
+
* It is per chain AND per asset value band, because the right depth for a $20
|
|
17
|
+
* transfer is not the right depth for a $2 million one. A single number is
|
|
18
|
+
* either too slow for small deposits or too shallow for large ones, and it is
|
|
19
|
+
* always the large one that costs money.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ChainError extends Error {
|
|
22
|
+
readonly code: 'UNKNOWN_CHAIN' | 'UNKNOWN_ASSET' | 'BAD_ADDRESS' | 'BAD_CONFIRMATIONS';
|
|
23
|
+
constructor(code: 'UNKNOWN_CHAIN' | 'UNKNOWN_ASSET' | 'BAD_ADDRESS' | 'BAD_CONFIRMATIONS', message: string);
|
|
24
|
+
}
|
|
25
|
+
export declare const ADDRESS_SCHEMES: readonly ["evm", "bitcoin", "tron", "solana"];
|
|
26
|
+
export type AddressScheme = (typeof ADDRESS_SCHEMES)[number];
|
|
27
|
+
/**
|
|
28
|
+
* How many confirmations a value band needs.
|
|
29
|
+
*
|
|
30
|
+
* Bands are open-ended upward: the last one applies to everything above its
|
|
31
|
+
* threshold. Ordered by `aboveUsd` ascending, and the lookup takes the LAST
|
|
32
|
+
* match — so adding a deeper band for larger amounts cannot accidentally
|
|
33
|
+
* shadow a shallower one.
|
|
34
|
+
*/
|
|
35
|
+
export interface ConfirmationBand {
|
|
36
|
+
/** Applies to transfers worth at least this much, in USD. */
|
|
37
|
+
aboveUsd: number;
|
|
38
|
+
confirmations: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ChainSpec {
|
|
41
|
+
key: string;
|
|
42
|
+
label: string;
|
|
43
|
+
scheme: AddressScheme;
|
|
44
|
+
/** EVM chain id. Absent for non-EVM chains, which have no such concept. */
|
|
45
|
+
chainId?: number;
|
|
46
|
+
/** The chain's own coin — the one gas is paid in. */
|
|
47
|
+
nativeAsset: string;
|
|
48
|
+
/** Seconds between blocks. Used to estimate how long a deposit will take. */
|
|
49
|
+
blockSeconds: number;
|
|
50
|
+
/**
|
|
51
|
+
* Ordered ascending by `aboveUsd`, first band starting at 0.
|
|
52
|
+
*
|
|
53
|
+
* The depth for a $20 transfer is not the depth for a $2 million one, and a
|
|
54
|
+
* single number is always wrong for one end.
|
|
55
|
+
*/
|
|
56
|
+
confirmations: readonly ConfirmationBand[];
|
|
57
|
+
/** `https://bscscan.com/tx/` — the trailing slash is part of it. */
|
|
58
|
+
explorerTx: string;
|
|
59
|
+
explorerAddress: string;
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface ChainAsset {
|
|
63
|
+
chain: string;
|
|
64
|
+
asset: string;
|
|
65
|
+
/** Absent for the chain's native coin. Present for every token. */
|
|
66
|
+
contract?: string;
|
|
67
|
+
/** Minimum worth depositing. Below this, gas to sweep it exceeds its value. */
|
|
68
|
+
minDeposit: string;
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Is this a plausible address on this chain?
|
|
73
|
+
*
|
|
74
|
+
* Shape only — it cannot tell a real address from a well-formed one that
|
|
75
|
+
* nobody holds. That limit is worth stating, because the check people want is
|
|
76
|
+
* "will this arrive", and no offline check answers it. What this does prevent
|
|
77
|
+
* is a withdrawal to an address from the WRONG chain, which is the common and
|
|
78
|
+
* unrecoverable mistake: an EVM address pasted into a Bitcoin withdrawal is
|
|
79
|
+
* refused here rather than broadcast into nothing.
|
|
80
|
+
*/
|
|
81
|
+
export declare function validateAddress(address: string, chain: ChainSpec): void;
|
|
82
|
+
export declare function assertChain(chain: ChainSpec): void;
|
|
83
|
+
/**
|
|
84
|
+
* How deep to wait for a transfer of this value.
|
|
85
|
+
*
|
|
86
|
+
* Takes the LAST matching band, so bands added later for larger amounts cannot
|
|
87
|
+
* shadow the ones below them.
|
|
88
|
+
*/
|
|
89
|
+
export declare function confirmationsFor(chain: ChainSpec, usdValue: number): number;
|
|
90
|
+
/** Roughly how long that will take. For a screen that says "about 4 minutes". */
|
|
91
|
+
export declare const estimatedSeconds: (chain: ChainSpec, usdValue: number) => number;
|
|
92
|
+
export declare function createChainRegistry(args: {
|
|
93
|
+
chains: readonly ChainSpec[];
|
|
94
|
+
assets: readonly ChainAsset[];
|
|
95
|
+
}): {
|
|
96
|
+
all: () => ChainSpec[];
|
|
97
|
+
byKey(key: string): ChainSpec;
|
|
98
|
+
/** Every asset that can be deposited on a chain. */
|
|
99
|
+
assetsOn: (chain: string) => ChainAsset[];
|
|
100
|
+
/** Every chain an asset can arrive on. USDT exists on several. */
|
|
101
|
+
chainsFor: (asset: string) => ChainSpec[];
|
|
102
|
+
asset(chain: string, asset: string): ChainAsset;
|
|
103
|
+
explorerTx: (chain: string, hash: string) => string;
|
|
104
|
+
explorerAddress: (chain: string, address: string) => string;
|
|
105
|
+
validate(chain: string, address: string): void;
|
|
106
|
+
};
|
|
107
|
+
export type ChainRegistry = ReturnType<typeof createChainRegistry>;
|
|
108
|
+
/**
|
|
109
|
+
* Sensible starting values, for a fresh install to edit.
|
|
110
|
+
*
|
|
111
|
+
* A SEED, not a source of truth — the registry is a store and this is what
|
|
112
|
+
* populates it the first time. The distinction matters: a constant that the
|
|
113
|
+
* code reads directly is a deploy every time a token is listed.
|
|
114
|
+
*/
|
|
115
|
+
export declare const SEED_CHAINS: readonly ChainSpec[];
|
|
116
|
+
export declare const SEED_CHAIN_ASSETS: readonly ChainAsset[];
|
|
@@ -0,0 +1,316 @@
|
|
|
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/chain.ts
|
|
155
|
+
class ChainError extends Error {
|
|
156
|
+
code;
|
|
157
|
+
constructor(code, message) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.code = code;
|
|
160
|
+
this.name = "ChainError";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
var ADDRESS_SCHEMES = ["evm", "bitcoin", "tron", "solana"];
|
|
164
|
+
var SCHEME_PATTERNS = {
|
|
165
|
+
evm: /^0x[a-fA-F0-9]{40}$/,
|
|
166
|
+
bitcoin: /^(bc1[a-z0-9]{25,62}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})$/,
|
|
167
|
+
tron: /^T[a-zA-Z0-9]{33}$/,
|
|
168
|
+
solana: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/
|
|
169
|
+
};
|
|
170
|
+
function validateAddress(address, chain) {
|
|
171
|
+
if (!SCHEME_PATTERNS[chain.scheme].test(address.trim())) {
|
|
172
|
+
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.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function assertChain(chain) {
|
|
176
|
+
if (chain.confirmations.length === 0) {
|
|
177
|
+
throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} declares no confirmation depth.`);
|
|
178
|
+
}
|
|
179
|
+
if (chain.confirmations[0].aboveUsd !== 0) {
|
|
180
|
+
throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} must have a band starting at 0, or small transfers match no rule.`);
|
|
181
|
+
}
|
|
182
|
+
let previous = -1;
|
|
183
|
+
for (const band of chain.confirmations) {
|
|
184
|
+
if (band.aboveUsd <= previous) {
|
|
185
|
+
throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} bands must ascend; ${band.aboveUsd} follows ${previous}.`);
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isInteger(band.confirmations) || band.confirmations < 1) {
|
|
188
|
+
throw new ChainError("BAD_CONFIRMATIONS", `${chain.key} band above $${band.aboveUsd} needs at least one confirmation.`);
|
|
189
|
+
}
|
|
190
|
+
previous = band.aboveUsd;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function confirmationsFor(chain, usdValue) {
|
|
194
|
+
let depth = chain.confirmations[0].confirmations;
|
|
195
|
+
for (const band of chain.confirmations) {
|
|
196
|
+
if (usdValue >= band.aboveUsd)
|
|
197
|
+
depth = band.confirmations;
|
|
198
|
+
}
|
|
199
|
+
return depth;
|
|
200
|
+
}
|
|
201
|
+
var estimatedSeconds = (chain, usdValue) => confirmationsFor(chain, usdValue) * chain.blockSeconds;
|
|
202
|
+
function createChainRegistry(args) {
|
|
203
|
+
for (const chain of args.chains)
|
|
204
|
+
assertChain(chain);
|
|
205
|
+
const chains = new Map(args.chains.map((chain) => [chain.key, chain]));
|
|
206
|
+
return {
|
|
207
|
+
all: () => [...chains.values()].filter((chain) => chain.enabled),
|
|
208
|
+
byKey(key) {
|
|
209
|
+
const chain = chains.get(key);
|
|
210
|
+
if (!chain)
|
|
211
|
+
throw new ChainError("UNKNOWN_CHAIN", `No chain "${key}" is configured.`);
|
|
212
|
+
return chain;
|
|
213
|
+
},
|
|
214
|
+
assetsOn: (chain) => args.assets.filter((asset) => asset.chain === chain && asset.enabled),
|
|
215
|
+
chainsFor: (asset) => args.assets.filter((entry) => entry.asset === asset && entry.enabled).map((entry) => chains.get(entry.chain)).filter((chain) => chain?.enabled === true),
|
|
216
|
+
asset(chain, asset) {
|
|
217
|
+
const entry = args.assets.find((row) => row.chain === chain && row.asset === asset);
|
|
218
|
+
if (!entry) {
|
|
219
|
+
throw new ChainError("UNKNOWN_ASSET", `${asset} is not configured on ${chain}.`);
|
|
220
|
+
}
|
|
221
|
+
assetSpec(asset);
|
|
222
|
+
return entry;
|
|
223
|
+
},
|
|
224
|
+
explorerTx: (chain, hash) => {
|
|
225
|
+
const spec = chains.get(chain);
|
|
226
|
+
return spec ? `${spec.explorerTx}${hash}` : hash;
|
|
227
|
+
},
|
|
228
|
+
explorerAddress: (chain, address) => {
|
|
229
|
+
const spec = chains.get(chain);
|
|
230
|
+
return spec ? `${spec.explorerAddress}${address}` : address;
|
|
231
|
+
},
|
|
232
|
+
validate(chain, address) {
|
|
233
|
+
validateAddress(address, this.byKey(chain));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var SEED_CHAINS = [
|
|
238
|
+
{
|
|
239
|
+
key: "bsc",
|
|
240
|
+
label: "BNB Smart Chain",
|
|
241
|
+
scheme: "evm",
|
|
242
|
+
chainId: 56,
|
|
243
|
+
nativeAsset: "BNB",
|
|
244
|
+
blockSeconds: 3,
|
|
245
|
+
confirmations: [
|
|
246
|
+
{ aboveUsd: 0, confirmations: 15 },
|
|
247
|
+
{ aboveUsd: 1e4, confirmations: 30 },
|
|
248
|
+
{ aboveUsd: 1e5, confirmations: 60 }
|
|
249
|
+
],
|
|
250
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
251
|
+
explorerAddress: "https://bscscan.com/address/",
|
|
252
|
+
enabled: true
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: "ethereum",
|
|
256
|
+
label: "Ethereum",
|
|
257
|
+
scheme: "evm",
|
|
258
|
+
chainId: 1,
|
|
259
|
+
nativeAsset: "ETH",
|
|
260
|
+
blockSeconds: 12,
|
|
261
|
+
confirmations: [
|
|
262
|
+
{ aboveUsd: 0, confirmations: 12 },
|
|
263
|
+
{ aboveUsd: 50000, confirmations: 32 }
|
|
264
|
+
],
|
|
265
|
+
explorerTx: "https://etherscan.io/tx/",
|
|
266
|
+
explorerAddress: "https://etherscan.io/address/",
|
|
267
|
+
enabled: true
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
key: "tron",
|
|
271
|
+
label: "Tron",
|
|
272
|
+
scheme: "tron",
|
|
273
|
+
nativeAsset: "TRX",
|
|
274
|
+
blockSeconds: 3,
|
|
275
|
+
confirmations: [{ aboveUsd: 0, confirmations: 20 }],
|
|
276
|
+
explorerTx: "https://tronscan.org/#/transaction/",
|
|
277
|
+
explorerAddress: "https://tronscan.org/#/address/",
|
|
278
|
+
enabled: true
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
var SEED_CHAIN_ASSETS = [
|
|
282
|
+
{ chain: "bsc", asset: "BNB", minDeposit: "0.001", enabled: true },
|
|
283
|
+
{
|
|
284
|
+
chain: "bsc",
|
|
285
|
+
asset: "USDT",
|
|
286
|
+
contract: "0x55d398326f99059ff775485246999027b3197955",
|
|
287
|
+
minDeposit: "1",
|
|
288
|
+
enabled: true
|
|
289
|
+
},
|
|
290
|
+
{ chain: "ethereum", asset: "ETH", minDeposit: "0.005", enabled: true },
|
|
291
|
+
{
|
|
292
|
+
chain: "ethereum",
|
|
293
|
+
asset: "USDT",
|
|
294
|
+
contract: "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
|
295
|
+
minDeposit: "20",
|
|
296
|
+
enabled: true
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
chain: "tron",
|
|
300
|
+
asset: "USDT",
|
|
301
|
+
contract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
302
|
+
minDeposit: "1",
|
|
303
|
+
enabled: true
|
|
304
|
+
}
|
|
305
|
+
];
|
|
306
|
+
export {
|
|
307
|
+
validateAddress,
|
|
308
|
+
estimatedSeconds,
|
|
309
|
+
createChainRegistry,
|
|
310
|
+
confirmationsFor,
|
|
311
|
+
assertChain,
|
|
312
|
+
SEED_CHAIN_ASSETS,
|
|
313
|
+
SEED_CHAINS,
|
|
314
|
+
ChainError,
|
|
315
|
+
ADDRESS_SCHEMES
|
|
316
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { allocate, type Money } from './money';
|
|
2
|
+
import { type Transaction, type AccountRef } from './ledger';
|
|
3
|
+
/**
|
|
4
|
+
* What the platform earns, and who it is shared with.
|
|
5
|
+
*
|
|
6
|
+
* Three rules from the owner, and each is one line of arithmetic sitting on top
|
|
7
|
+
* of primitives that already exist:
|
|
8
|
+
*
|
|
9
|
+
* COMMISSION a share of the tenant's PROFIT for the period, at the tier rate
|
|
10
|
+
* REFERRAL a share of THAT — of our income, not of their profit — to
|
|
11
|
+
* whoever introduced them
|
|
12
|
+
* PREPAY higher tiers pay half the projected term commission up front
|
|
13
|
+
*
|
|
14
|
+
* ## Profit, not volume
|
|
15
|
+
*
|
|
16
|
+
* The distinction is the whole basis of the charge and it is easy to get wrong
|
|
17
|
+
* under pressure: a period where a tenant traded ten million and lost money
|
|
18
|
+
* owes nothing. Commission on a losing period would be a fee dressed as a
|
|
19
|
+
* share, and the first tenant to notice would be right to leave.
|
|
20
|
+
*
|
|
21
|
+
* ## Losses carry forward
|
|
22
|
+
*
|
|
23
|
+
* A tenant who loses 50 in one week and makes 60 the next is not up 60 — they
|
|
24
|
+
* are up 10, and charging on 60 charges them for recovering their own money.
|
|
25
|
+
* The high-water mark makes that structural rather than a policy somebody has
|
|
26
|
+
* to remember.
|
|
27
|
+
*/
|
|
28
|
+
export declare class CommissionError extends Error {
|
|
29
|
+
readonly code: 'BAD_RATE' | 'NO_TIER' | 'ASSET_MISMATCH' | 'NEGATIVE_PERIOD';
|
|
30
|
+
constructor(code: 'BAD_RATE' | 'NO_TIER' | 'ASSET_MISMATCH' | 'NEGATIVE_PERIOD', message: string);
|
|
31
|
+
}
|
|
32
|
+
export interface PeriodPerformance {
|
|
33
|
+
/** Equity at the start of the period, in the account's asset. */
|
|
34
|
+
openingEquity: Money;
|
|
35
|
+
closingEquity: Money;
|
|
36
|
+
/** Deposits during the period. Not profit — the tenant's own money. */
|
|
37
|
+
deposits: Money;
|
|
38
|
+
withdrawals: Money;
|
|
39
|
+
/**
|
|
40
|
+
* The highest equity ever charged on, net of flows.
|
|
41
|
+
*
|
|
42
|
+
* Losses carry forward against this, so a recovery is not charged twice.
|
|
43
|
+
*/
|
|
44
|
+
highWaterMark: Money;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Profit for one period, adjusted for money moving in and out.
|
|
48
|
+
*
|
|
49
|
+
* profit = closing − opening − deposits + withdrawals
|
|
50
|
+
*
|
|
51
|
+
* Without the flow adjustment a tenant depositing a million looks like a
|
|
52
|
+
* million of profit, and the invoice that follows would be indefensible. This
|
|
53
|
+
* is the single most important line in the file.
|
|
54
|
+
*/
|
|
55
|
+
export declare function periodProfit(performance: PeriodPerformance): Money;
|
|
56
|
+
/**
|
|
57
|
+
* The part of the profit that is actually chargeable.
|
|
58
|
+
*
|
|
59
|
+
* Zero when the account is still below its high-water mark. A tenant who lost
|
|
60
|
+
* 50 and made 60 owes on 10, not on 60 — charging on the recovery charges them
|
|
61
|
+
* for getting their own money back.
|
|
62
|
+
*/
|
|
63
|
+
export declare function chargeableProfit(performance: PeriodPerformance): Money;
|
|
64
|
+
/** The new mark after a period. Never falls — that is what "high water" means. */
|
|
65
|
+
export declare const nextHighWaterMark: (performance: PeriodPerformance) => Money;
|
|
66
|
+
export interface CommissionInput {
|
|
67
|
+
profit: Money;
|
|
68
|
+
/** Platform share of profit. `'0.30'` is thirty per cent. */
|
|
69
|
+
commissionRate: string;
|
|
70
|
+
/** Referrer share of OUR income — not of the profit. */
|
|
71
|
+
referralRate?: string;
|
|
72
|
+
referrer?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface CommissionSplit {
|
|
75
|
+
/** What the tenant owes. */
|
|
76
|
+
total: Money;
|
|
77
|
+
/** What the platform keeps after the referral share. */
|
|
78
|
+
platform: Money;
|
|
79
|
+
referral: Money;
|
|
80
|
+
referrer?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Split a period's profit into what is owed and how it is shared.
|
|
84
|
+
*
|
|
85
|
+
* `allocate` does the division, so the parts always sum back to the total. A
|
|
86
|
+
* split that loses a unit is a transaction that cannot be posted, because the
|
|
87
|
+
* ledger refuses anything that does not balance — which is the correct place
|
|
88
|
+
* for that to fail.
|
|
89
|
+
*/
|
|
90
|
+
export declare function splitCommission(input: CommissionInput): CommissionSplit;
|
|
91
|
+
/**
|
|
92
|
+
* The split as ledger entries.
|
|
93
|
+
*
|
|
94
|
+
* One transaction with three legs, so it balances or it does not post. A
|
|
95
|
+
* referral paid by a second transaction could succeed while the first failed,
|
|
96
|
+
* which is how a referrer gets paid on income the platform never collected.
|
|
97
|
+
*/
|
|
98
|
+
export declare function commissionTransaction(args: {
|
|
99
|
+
reference: string;
|
|
100
|
+
owner: string;
|
|
101
|
+
split: CommissionSplit;
|
|
102
|
+
atMs: number;
|
|
103
|
+
revenueAccount?: AccountRef;
|
|
104
|
+
}): Transaction | null;
|
|
105
|
+
export interface PrepayInput {
|
|
106
|
+
/** What the tenant is subscribing with. */
|
|
107
|
+
subscriptionAmount: Money;
|
|
108
|
+
/** Expected return over the term, as a rate. `'0.20'` is twenty per cent. */
|
|
109
|
+
projectedReturnRate: string;
|
|
110
|
+
commissionRate: string;
|
|
111
|
+
/** Fraction of the projected commission payable now. The owner's rule is a half. */
|
|
112
|
+
prepayRate: string;
|
|
113
|
+
}
|
|
114
|
+
export interface Prepay {
|
|
115
|
+
/** What the term is projected to yield the tenant. */
|
|
116
|
+
projectedProfit: Money;
|
|
117
|
+
/** Commission on that, if the projection holds. */
|
|
118
|
+
projectedCommission: Money;
|
|
119
|
+
/** Payable up front. */
|
|
120
|
+
due: Money;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What a higher tier pays before the term starts.
|
|
124
|
+
*
|
|
125
|
+
* Every input is a rate the operator sets, including the projection — this is
|
|
126
|
+
* arithmetic on stated assumptions, not a forecast. Naming it `projected`
|
|
127
|
+
* everywhere is deliberate: a figure derived from an assumed return should
|
|
128
|
+
* never read on a screen as an amount somebody has earned.
|
|
129
|
+
*
|
|
130
|
+
* The prepay is a CREDIT against real commission, not an extra charge. That
|
|
131
|
+
* belongs to whoever settles the invoice, and the shape here makes it easy to
|
|
132
|
+
* do: `due` is in the same asset as the commission it offsets.
|
|
133
|
+
*/
|
|
134
|
+
export declare function projectPrepay(input: PrepayInput): Prepay;
|
|
135
|
+
export interface ReferralShare {
|
|
136
|
+
referrer: string;
|
|
137
|
+
rate: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Split one income figure across several referrers.
|
|
141
|
+
*
|
|
142
|
+
* Multi-level is not currently used and the shape supports it anyway, because
|
|
143
|
+
* retrofitting a second level means changing how every historical commission
|
|
144
|
+
* was computed. `allocate` guarantees the parts sum to the whole, so the
|
|
145
|
+
* platform's remainder absorbs any rounding rather than a unit vanishing.
|
|
146
|
+
*/
|
|
147
|
+
export declare function splitReferrals(income: Money, shares: readonly ReferralShare[]): {
|
|
148
|
+
platform: Money;
|
|
149
|
+
paid: {
|
|
150
|
+
referrer: string;
|
|
151
|
+
amount: Money;
|
|
152
|
+
}[];
|
|
153
|
+
};
|
|
154
|
+
/** Even split of a remainder, losing nothing. Re-exported so callers need one import. */
|
|
155
|
+
export { allocate };
|