@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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact amounts, with the asset attached.
|
|
3
|
+
*
|
|
4
|
+
* Two mistakes account for almost every money bug, and both are eliminated here
|
|
5
|
+
* by construction rather than by care:
|
|
6
|
+
*
|
|
7
|
+
* FLOATS `0.1 + 0.2` is not `0.3`, and a double cannot represent satoshi
|
|
8
|
+
* precision above roughly ninety million units at all. The error
|
|
9
|
+
* is small, real, and accumulates into a ledger that fails to
|
|
10
|
+
* balance by a few units a month with nobody able to say why.
|
|
11
|
+
* Amounts here are `bigint` minor units — never a `number`.
|
|
12
|
+
*
|
|
13
|
+
* MIXED UNITS adding BTC to USDT type-checks perfectly if both are numbers.
|
|
14
|
+
* The asset travels WITH the amount and every operation refuses a
|
|
15
|
+
* mismatch, so the mistake is a thrown error at the point it is
|
|
16
|
+
* made rather than a plausible figure somewhere downstream.
|
|
17
|
+
*
|
|
18
|
+
* ## Why not a decimal library
|
|
19
|
+
*
|
|
20
|
+
* Arbitrary-precision decimals solve the float problem and not the unit one,
|
|
21
|
+
* and they invite `0.1` back into the code as a literal. Minor units mean the
|
|
22
|
+
* only representable values are the ones the asset actually has — you cannot
|
|
23
|
+
* express half a satoshi, because there is no such thing.
|
|
24
|
+
*
|
|
25
|
+
* ## Rounding is never implicit
|
|
26
|
+
*
|
|
27
|
+
* Every operation that could lose precision takes an explicit mode. A default
|
|
28
|
+
* would be a decision made once, silently, in whichever direction happened to
|
|
29
|
+
* suit the first caller — and on the wrong side it is theft in one direction
|
|
30
|
+
* and a loss in the other.
|
|
31
|
+
*/
|
|
32
|
+
export declare class MoneyError extends Error {
|
|
33
|
+
readonly code: 'ASSET_MISMATCH' | 'UNKNOWN_ASSET' | 'NOT_FINITE' | 'PRECISION_LOSS' | 'DIVIDE_BY_ZERO';
|
|
34
|
+
constructor(code: 'ASSET_MISMATCH' | 'UNKNOWN_ASSET' | 'NOT_FINITE' | 'PRECISION_LOSS' | 'DIVIDE_BY_ZERO', message: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* How many minor units make one whole unit.
|
|
38
|
+
*
|
|
39
|
+
* Declared per asset because it is a property of the asset and not a display
|
|
40
|
+
* preference: USDT has 6 on most chains, BTC has 8, ETH has 18. Getting it
|
|
41
|
+
* wrong by one decimal place is a factor-of-ten error in a balance.
|
|
42
|
+
*/
|
|
43
|
+
export interface AssetSpec {
|
|
44
|
+
code: string;
|
|
45
|
+
decimals: number;
|
|
46
|
+
/** What to show a human. `USDT`, `BTC` — never a symbol that collides. */
|
|
47
|
+
label?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare const ASSETS: readonly AssetSpec[];
|
|
50
|
+
/** Register an asset the built-in table does not carry. */
|
|
51
|
+
export declare function defineAsset(spec: AssetSpec): void;
|
|
52
|
+
export declare function assetSpec(code: string): AssetSpec;
|
|
53
|
+
/** An amount and what it is denominated in. Immutable. */
|
|
54
|
+
export interface Money {
|
|
55
|
+
/** Minor units. 1 USDT is 1_000_000n at 6 decimals. */
|
|
56
|
+
readonly units: bigint;
|
|
57
|
+
readonly asset: string;
|
|
58
|
+
}
|
|
59
|
+
export declare const money: (units: bigint, asset: string) => Money;
|
|
60
|
+
export declare const zero: (asset: string) => Money;
|
|
61
|
+
/**
|
|
62
|
+
* Parse a human string into exact minor units.
|
|
63
|
+
*
|
|
64
|
+
* Refuses more precision than the asset has rather than rounding it away.
|
|
65
|
+
* "0.0000000001 BTC" is not a very small amount of bitcoin — it is a mistake,
|
|
66
|
+
* usually a units error, and silently truncating it hides the bug that produced
|
|
67
|
+
* it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseAmount(value: string, asset: string): Money;
|
|
70
|
+
/** Render minor units as a human string. Exact — never a float on the way out. */
|
|
71
|
+
export declare function formatAmount(amount: Money, options?: {
|
|
72
|
+
trim?: boolean;
|
|
73
|
+
}): string;
|
|
74
|
+
export declare function add(a: Money, b: Money): Money;
|
|
75
|
+
export declare function subtract(a: Money, b: Money): Money;
|
|
76
|
+
export declare const negate: (amount: Money) => Money;
|
|
77
|
+
export declare const abs: (amount: Money) => Money;
|
|
78
|
+
export declare const isZero: (amount: Money) => boolean;
|
|
79
|
+
export declare const isNegative: (amount: Money) => boolean;
|
|
80
|
+
export declare function compare(a: Money, b: Money): -1 | 0 | 1;
|
|
81
|
+
export declare const equals: (a: Money, b: Money) => boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Rounding modes, named for what they do to the HOLDER of the money.
|
|
84
|
+
*
|
|
85
|
+
* `down` truncates toward zero — the safe default for anything a venue will
|
|
86
|
+
* check, because rounding a quantity up can exceed a balance and get the whole
|
|
87
|
+
* order rejected.
|
|
88
|
+
*/
|
|
89
|
+
export declare const ROUNDING: readonly ["down", "up", "half-up"];
|
|
90
|
+
export type Rounding = (typeof ROUNDING)[number];
|
|
91
|
+
/**
|
|
92
|
+
* Multiply by a rate expressed as a decimal string.
|
|
93
|
+
*
|
|
94
|
+
* The rate is a STRING, not a number: writing `0.001` as a literal reintroduces
|
|
95
|
+
* the float this module exists to keep out, and a price like `43210.55` is
|
|
96
|
+
* exactly the value a double gets subtly wrong.
|
|
97
|
+
*/
|
|
98
|
+
export declare function mulRate(amount: Money, rate: string, mode?: Rounding): Money;
|
|
99
|
+
/**
|
|
100
|
+
* Convert between assets at a stated rate.
|
|
101
|
+
*
|
|
102
|
+
* The rate is "how many units of `to` per one whole unit of `from`", which is
|
|
103
|
+
* how every venue and price feed quotes it. Doing the decimal adjustment here
|
|
104
|
+
* rather than at each call site is the point — it is the step that is easy to
|
|
105
|
+
* get wrong by a factor of a thousand when the two assets differ in precision.
|
|
106
|
+
*/
|
|
107
|
+
export declare function convert(amount: Money, to: string, rate: string, mode?: Rounding): Money;
|
|
108
|
+
/**
|
|
109
|
+
* Split an amount into parts without losing a unit.
|
|
110
|
+
*
|
|
111
|
+
* The remainder is distributed one unit at a time across the first parts rather
|
|
112
|
+
* than dropped, so the parts always sum back to the original. A split that
|
|
113
|
+
* loses units is how a ledger drifts by a few cents a day.
|
|
114
|
+
*/
|
|
115
|
+
export declare function allocate(amount: Money, parts: number): Money[];
|
|
116
|
+
/** Round to a venue's step size — a lot size or a tick. Down, so it still fits. */
|
|
117
|
+
export declare function toStep(amount: Money, step: string, mode?: Rounding): Money;
|
|
118
|
+
export declare const VERSION = "0.1.0";
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
export {
|
|
154
|
+
zero,
|
|
155
|
+
toStep,
|
|
156
|
+
subtract,
|
|
157
|
+
parseAmount,
|
|
158
|
+
negate,
|
|
159
|
+
mulRate,
|
|
160
|
+
money,
|
|
161
|
+
isZero,
|
|
162
|
+
isNegative,
|
|
163
|
+
formatAmount,
|
|
164
|
+
equals,
|
|
165
|
+
defineAsset,
|
|
166
|
+
convert,
|
|
167
|
+
compare,
|
|
168
|
+
assetSpec,
|
|
169
|
+
allocate,
|
|
170
|
+
add,
|
|
171
|
+
abs,
|
|
172
|
+
VERSION,
|
|
173
|
+
ROUNDING,
|
|
174
|
+
MoneyError,
|
|
175
|
+
ASSETS
|
|
176
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { type Money } from './money';
|
|
2
|
+
/**
|
|
3
|
+
* What an asset is worth, and how old that answer is.
|
|
4
|
+
*
|
|
5
|
+
* The platform keeps its books in USD, so every balance in BTC, ETH or a token
|
|
6
|
+
* has to be expressed in one unit before it can be added to anything. That
|
|
7
|
+
* needs a rate, and a rate needs two things people routinely leave out:
|
|
8
|
+
*
|
|
9
|
+
* A SOURCE a peg is not a quote. USDT is 1 by definition and asking an
|
|
10
|
+
* exchange for it introduces an error where none existed — a
|
|
11
|
+
* stablecoin trading at 0.9994 would silently revalue every
|
|
12
|
+
* balance on the platform by six basis points.
|
|
13
|
+
* AN AGE a rate with no timestamp is a rate that keeps quoting yesterday
|
|
14
|
+
* after the feed dies. Nothing here returns a value without also
|
|
15
|
+
* saying when it was true, and a caller pricing a withdrawal can
|
|
16
|
+
* refuse a stale one.
|
|
17
|
+
*
|
|
18
|
+
* ## Why the conversion itself is not implemented here
|
|
19
|
+
*
|
|
20
|
+
* `./money` already has `convert`, and the hard part is not
|
|
21
|
+
* the multiplication — it is the decimal adjustment between assets of different
|
|
22
|
+
* precision, which is where a factor-of-a-thousand error comes from. That is
|
|
23
|
+
* written and tested once. This module answers *what rate*, never *how to
|
|
24
|
+
* apply it*.
|
|
25
|
+
*/
|
|
26
|
+
export declare class RateError extends Error {
|
|
27
|
+
readonly code: 'NO_RATE' | 'STALE_RATE' | 'BAD_RATE' | 'UNKNOWN_ASSET';
|
|
28
|
+
readonly asset?: string | undefined;
|
|
29
|
+
constructor(code: 'NO_RATE' | 'STALE_RATE' | 'BAD_RATE' | 'UNKNOWN_ASSET', message: string, asset?: string | undefined);
|
|
30
|
+
}
|
|
31
|
+
/** The unit the platform keeps its books in. */
|
|
32
|
+
export declare const BASE_ASSET = "USD";
|
|
33
|
+
export declare const RATE_SOURCES: readonly ["peg", "venue", "manual"];
|
|
34
|
+
export type RateSource = (typeof RATE_SOURCES)[number];
|
|
35
|
+
/**
|
|
36
|
+
* How an asset is priced.
|
|
37
|
+
*
|
|
38
|
+
* peg fixed by definition and never fetched. USDT is 1.
|
|
39
|
+
* venue quoted live from a chosen source, and therefore has an age.
|
|
40
|
+
* manual an operator typed it. Also has an age, and deliberately so: a
|
|
41
|
+
* manual rate somebody set in March is more dangerous than a missing
|
|
42
|
+
* one, because it looks maintained.
|
|
43
|
+
*/
|
|
44
|
+
export interface AssetRate {
|
|
45
|
+
asset: string;
|
|
46
|
+
source: RateSource;
|
|
47
|
+
/** USD per ONE whole unit, as a decimal string. Never a float. */
|
|
48
|
+
usd: string;
|
|
49
|
+
/** When this was true. Absent only for a peg, which has no such moment. */
|
|
50
|
+
atMs?: number;
|
|
51
|
+
/** For `venue`: which market it came from, so a wrong price is traceable. */
|
|
52
|
+
via?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface AssetEntry {
|
|
55
|
+
code: string;
|
|
56
|
+
label: string;
|
|
57
|
+
decimals: number;
|
|
58
|
+
/**
|
|
59
|
+
* Chain this asset lives on, when it is a token.
|
|
60
|
+
*
|
|
61
|
+
* Absent for fiat and for a chain's own coin, which is why the field is
|
|
62
|
+
* optional rather than a string that sometimes says "native" — a sentinel
|
|
63
|
+
* value in a foreign key is a join nobody can write.
|
|
64
|
+
*/
|
|
65
|
+
chain?: string;
|
|
66
|
+
/** Contract address for a token. Absent for a native coin. */
|
|
67
|
+
contract?: string;
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
rate: AssetRate;
|
|
70
|
+
}
|
|
71
|
+
export declare function assertRate(rate: AssetRate): void;
|
|
72
|
+
/** USDT and USDC at exactly 1, by definition rather than by quote. */
|
|
73
|
+
export declare const peg: (asset: string, usd?: string) => AssetRate;
|
|
74
|
+
export interface RateTableOptions {
|
|
75
|
+
/** How old a quoted rate may be before it is refused. Default 15 minutes. */
|
|
76
|
+
maxAgeMs?: number;
|
|
77
|
+
now?: () => number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The rates in force, and the one place that decides a quote is too old.
|
|
81
|
+
*
|
|
82
|
+
* Staleness is checked on READ rather than swept on a timer. A sweeper that
|
|
83
|
+
* deletes old rates turns a stale-price incident into a missing-price incident
|
|
84
|
+
* — the same outage with less information — and it can itself fail, leaving
|
|
85
|
+
* rates that look fresh because nothing removed them.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createRateTable(options?: RateTableOptions): {
|
|
88
|
+
set(rate: AssetRate): void;
|
|
89
|
+
/** The raw entry, however old. For an admin screen that must show staleness. */
|
|
90
|
+
peek: (asset: string) => AssetRate | undefined;
|
|
91
|
+
ageOf(asset: string): number | undefined;
|
|
92
|
+
isStale(asset: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The rate, or a refusal that says which problem it is.
|
|
95
|
+
*
|
|
96
|
+
* Missing and stale are different failures with different fixes — one is
|
|
97
|
+
* an unconfigured asset, the other a dead feed — and collapsing them sends
|
|
98
|
+
* an operator to the wrong screen.
|
|
99
|
+
*
|
|
100
|
+
* Every valuation below goes through THIS, so the staleness rule exists in
|
|
101
|
+
* exactly one place. Two copies of it is two chances for a valuation path
|
|
102
|
+
* to accept a rate another path would refuse.
|
|
103
|
+
*/
|
|
104
|
+
require(asset: string): AssetRate;
|
|
105
|
+
/**
|
|
106
|
+
* Value an amount in USD.
|
|
107
|
+
*
|
|
108
|
+
* Rounded DOWN. A rounding decision on a platform balance should leave the
|
|
109
|
+
* platform conservative about what it holds — a thousand rounded-up
|
|
110
|
+
* valuations become a shortfall nobody can locate.
|
|
111
|
+
*/
|
|
112
|
+
toUsd(amount: Money): Money;
|
|
113
|
+
/** Sum a mixed bag of assets into one USD figure. */
|
|
114
|
+
totalUsd(amounts: readonly Money[]): Money;
|
|
115
|
+
all: () => AssetRate[];
|
|
116
|
+
clear: () => void;
|
|
117
|
+
};
|
|
118
|
+
export type RateTable = ReturnType<typeof createRateTable>;
|
|
119
|
+
/**
|
|
120
|
+
* Register an asset so `money` knows its precision.
|
|
121
|
+
*
|
|
122
|
+
* Called when an admin adds one. Getting `decimals` wrong is a factor-of-ten
|
|
123
|
+
* error in every balance of that asset, which is why it is declared per asset
|
|
124
|
+
* rather than defaulted — there is no safe default.
|
|
125
|
+
*/
|
|
126
|
+
export declare function registerAsset(entry: AssetEntry): void;
|
|
127
|
+
export declare const isRegistered: (code: string) => boolean;
|
|
128
|
+
/**
|
|
129
|
+
* A rate fetched from a venue.
|
|
130
|
+
*
|
|
131
|
+
* The fetcher is injected rather than imported, so this module stays free of a
|
|
132
|
+
* transport and a test needs no network. The venue adapter already carries the
|
|
133
|
+
* weight budget, which is the reason not to open a second HTTP path here.
|
|
134
|
+
*/
|
|
135
|
+
export interface RateFetcher {
|
|
136
|
+
/** USD price of one whole unit, as a decimal string, or undefined if unquoted. */
|
|
137
|
+
quote(asset: string): Promise<string | undefined>;
|
|
138
|
+
}
|
|
139
|
+
export interface RefreshReport {
|
|
140
|
+
refreshed: string[];
|
|
141
|
+
/** Assets whose quote failed. The PREVIOUS rate is left in place — see below. */
|
|
142
|
+
failed: {
|
|
143
|
+
asset: string;
|
|
144
|
+
reason: string;
|
|
145
|
+
}[];
|
|
146
|
+
skipped: string[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Refresh every quoted asset.
|
|
150
|
+
*
|
|
151
|
+
* A failed quote leaves the old rate alone rather than clearing it. Clearing
|
|
152
|
+
* would turn a brief feed outage into "no rate configured", which reads as a
|
|
153
|
+
* misconfiguration and sends somebody to the wrong screen — and the old rate is
|
|
154
|
+
* still visibly ageing, so `require()` refuses it on its own once it passes the
|
|
155
|
+
* limit. Doing nothing is the correct action; doing nothing SILENTLY is not,
|
|
156
|
+
* which is why failures are reported.
|
|
157
|
+
*/
|
|
158
|
+
export declare function refreshRates(table: RateTable, fetcher: RateFetcher, args: {
|
|
159
|
+
assets: readonly AssetEntry[];
|
|
160
|
+
now?: () => number;
|
|
161
|
+
}): Promise<RefreshReport>;
|
|
162
|
+
/** The refresh as a job spec, for `@forgezero/runtime/jobs`. */
|
|
163
|
+
export declare function rateRefreshJob(args: {
|
|
164
|
+
table: RateTable;
|
|
165
|
+
fetcher: RateFetcher;
|
|
166
|
+
assets: () => readonly AssetEntry[];
|
|
167
|
+
everyMs?: number;
|
|
168
|
+
key?: string;
|
|
169
|
+
}): {
|
|
170
|
+
key: string;
|
|
171
|
+
everyMs: number;
|
|
172
|
+
run: () => Promise<{
|
|
173
|
+
ok: boolean;
|
|
174
|
+
detail: RefreshReport;
|
|
175
|
+
}>;
|
|
176
|
+
};
|
|
177
|
+
/** For an admin screen: what each asset is worth and how much to trust it. */
|
|
178
|
+
export declare const describeRate: (rate: AssetRate, ageMs?: number) => string;
|