@forgezero/runtime 0.1.14 → 0.1.16
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/jobs.d.ts +5 -6
- 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 +3 -62
- package/contracts/foundry.toml +0 -9
- package/contracts/src/ColdVault.sol +0 -206
- package/contracts/src/DepositFactory.sol +0 -202
- package/contracts/src/DepositProxy.sol +0 -72
- package/contracts/src/IERC20.sol +0 -7
- package/contracts/src/MockTokens.sol +0 -32
- package/contracts/src/SafeTransferLib.sol +0 -31
- package/contracts/test/Custody.t.sol +0 -361
- package/contracts/test/Vectors.t.sol +0 -45
- 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,292 +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/transfers.ts
|
|
155
|
-
import { Refusal, isRefusal } from "@forgezero/access";
|
|
156
|
-
var DIRECTIONS = ["deposit", "withdrawal"];
|
|
157
|
-
var SCREENING_ORDER = 0;
|
|
158
|
-
|
|
159
|
-
class PipelineError extends Error {
|
|
160
|
-
code;
|
|
161
|
-
constructor(code, message) {
|
|
162
|
-
super(message);
|
|
163
|
-
this.code = code;
|
|
164
|
-
this.name = "PipelineError";
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function createTransferPipeline(options) {
|
|
168
|
-
const seen = new Set;
|
|
169
|
-
for (const stage of options.stages) {
|
|
170
|
-
if (seen.has(stage.name)) {
|
|
171
|
-
throw new PipelineError("DUPLICATE_STAGE", `Two stages are named "${stage.name}".`);
|
|
172
|
-
}
|
|
173
|
-
seen.add(stage.name);
|
|
174
|
-
}
|
|
175
|
-
if (options.requireScreening && !options.stages.some((stage) => stage.order === SCREENING_ORDER)) {
|
|
176
|
-
throw new PipelineError("NO_SCREENING", "This pipeline requires a screening stage at order 0 and has none. Register one, or set requireScreening to false and accept that transfers are unscreened.");
|
|
177
|
-
}
|
|
178
|
-
const ordered = [...options.stages].sort((a, b) => a.order - b.order);
|
|
179
|
-
const now = options.now ?? Date.now;
|
|
180
|
-
return {
|
|
181
|
-
stages: ordered.map((stage) => ({ name: stage.name, order: stage.order })),
|
|
182
|
-
async run(transfer) {
|
|
183
|
-
const outcomes = [];
|
|
184
|
-
let current = transfer;
|
|
185
|
-
for (const stage of ordered) {
|
|
186
|
-
if (stage.directions && !stage.directions.includes(current.direction))
|
|
187
|
-
continue;
|
|
188
|
-
const startedAt = now();
|
|
189
|
-
try {
|
|
190
|
-
const patch = await stage.run(current);
|
|
191
|
-
if (patch)
|
|
192
|
-
current = { ...current, context: { ...current.context, ...patch } };
|
|
193
|
-
const outcome = { stage: stage.name, ok: true, ms: now() - startedAt };
|
|
194
|
-
outcomes.push(outcome);
|
|
195
|
-
options.onStage?.(outcome, current);
|
|
196
|
-
} catch (cause) {
|
|
197
|
-
const refusal = isRefusal(cause) ? cause : new Refusal(500, "STAGE_FAILED", `The "${stage.name}" check could not complete.`, { stage: stage.name }, true);
|
|
198
|
-
const outcome = {
|
|
199
|
-
stage: stage.name,
|
|
200
|
-
ok: false,
|
|
201
|
-
ms: now() - startedAt,
|
|
202
|
-
code: refusal.code
|
|
203
|
-
};
|
|
204
|
-
outcomes.push(outcome);
|
|
205
|
-
options.onStage?.(outcome, current);
|
|
206
|
-
return { ok: false, transfer: current, stages: outcomes, refusal };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return { ok: true, transfer: current, stages: outcomes };
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
var screeningStage = (check = () => true) => ({
|
|
214
|
-
name: "aml.screening",
|
|
215
|
-
order: SCREENING_ORDER,
|
|
216
|
-
async run(transfer) {
|
|
217
|
-
const allowed = await check(transfer);
|
|
218
|
-
if (!allowed) {
|
|
219
|
-
throw new Refusal(451, "SCREENING_REFUSED", "This transfer cannot be processed.", {
|
|
220
|
-
direction: transfer.direction
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
return { screenedAtMs: transfer.atMs };
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
var limitsStage = (limits) => ({
|
|
227
|
-
name: "limits",
|
|
228
|
-
order: 10,
|
|
229
|
-
directions: limits.directions,
|
|
230
|
-
run(transfer) {
|
|
231
|
-
if (transfer.amount.units <= 0n) {
|
|
232
|
-
throw new Refusal(422, "AMOUNT_INVALID", "The amount must be positive.");
|
|
233
|
-
}
|
|
234
|
-
if (limits.min && transfer.amount.asset === limits.min.asset && transfer.amount.units < limits.min.units) {
|
|
235
|
-
throw new Refusal(422, "BELOW_MINIMUM", `The minimum is ${formatAmount(limits.min, { trim: true })} ${limits.min.asset}.`);
|
|
236
|
-
}
|
|
237
|
-
if (limits.max && transfer.amount.asset === limits.max.asset && transfer.amount.units > limits.max.units) {
|
|
238
|
-
throw new Refusal(422, "ABOVE_MAXIMUM", `The maximum is ${formatAmount(limits.max, { trim: true })} ${limits.max.asset}.`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
var balanceStage = (available) => ({
|
|
243
|
-
name: "balance",
|
|
244
|
-
order: 20,
|
|
245
|
-
directions: ["withdrawal"],
|
|
246
|
-
async run(transfer) {
|
|
247
|
-
const free = await available(transfer);
|
|
248
|
-
if (free.units < transfer.amount.units) {
|
|
249
|
-
throw new Refusal(409, "INSUFFICIENT_BALANCE", "The available balance does not cover this.", {
|
|
250
|
-
required: formatAmount(transfer.amount, { trim: true }),
|
|
251
|
-
available: formatAmount(free, { trim: true }),
|
|
252
|
-
asset: transfer.amount.asset
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
var approvalStage = (args) => ({
|
|
258
|
-
name: "approval",
|
|
259
|
-
order: 30,
|
|
260
|
-
directions: ["withdrawal"],
|
|
261
|
-
async run(transfer) {
|
|
262
|
-
if (transfer.amount.asset !== args.threshold.asset)
|
|
263
|
-
return;
|
|
264
|
-
if (transfer.amount.units < args.threshold.units)
|
|
265
|
-
return;
|
|
266
|
-
if (!await args.isApproved(transfer)) {
|
|
267
|
-
throw new Refusal(409, "AWAITING_APPROVAL", "This transfer is held for review.", {
|
|
268
|
-
threshold: formatAmount(args.threshold, { trim: true })
|
|
269
|
-
}, true);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
var notFrozenStage = (isFrozen) => ({
|
|
274
|
-
name: "account.active",
|
|
275
|
-
order: 5,
|
|
276
|
-
async run(transfer) {
|
|
277
|
-
if (await isFrozen(transfer.owner)) {
|
|
278
|
-
throw new Refusal(423, "ACCOUNT_FROZEN", "This account is suspended. Settle the outstanding invoice to resume.");
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
export {
|
|
283
|
-
screeningStage,
|
|
284
|
-
notFrozenStage,
|
|
285
|
-
limitsStage,
|
|
286
|
-
createTransferPipeline,
|
|
287
|
-
balanceStage,
|
|
288
|
-
approvalStage,
|
|
289
|
-
SCREENING_ORDER,
|
|
290
|
-
PipelineError,
|
|
291
|
-
DIRECTIONS
|
|
292
|
-
};
|
package/dist/finance/venues.d.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { type Money } from './money';
|
|
2
|
-
/**
|
|
3
|
-
* Trading venues, market types and symbols — as data.
|
|
4
|
-
*
|
|
5
|
-
* A trading system accumulates venue knowledge in the worst possible places:
|
|
6
|
-
* a lot size hard-coded in an order builder, a minimum notional in a validator,
|
|
7
|
-
* a symbol spelling in three files that disagree. This declares it once, so
|
|
8
|
-
* adding a market is data and adding a venue is one adapter.
|
|
9
|
-
*
|
|
10
|
-
* ## Why spot, margin and futures share an order model
|
|
11
|
-
*
|
|
12
|
-
* They differ in what they settle against, not in what an order IS. All three
|
|
13
|
-
* take a symbol, a side, a quantity and optionally a price; all three reject
|
|
14
|
-
* quantities off the lot step and notionals under a minimum. Modelling them
|
|
15
|
-
* separately means writing the same validation three times and getting it
|
|
16
|
-
* subtly different — and the difference will be in the one nobody trades often.
|
|
17
|
-
*
|
|
18
|
-
* What genuinely differs is expressed as fields on the market: leverage exists
|
|
19
|
-
* on margin and futures and not on spot, futures carry a contract type and an
|
|
20
|
-
* expiry. A caller that ignores those still cannot place an invalid order,
|
|
21
|
-
* because validation reads the market rather than the caller's intent.
|
|
22
|
-
*
|
|
23
|
-
* ## Nothing here talks to a venue
|
|
24
|
-
*
|
|
25
|
-
* `VenueAdapter` is an interface. This package validates, normalises and
|
|
26
|
-
* describes; the transport lives with whoever implements the adapter, so a new
|
|
27
|
-
* venue costs one file and no changes here.
|
|
28
|
-
*/
|
|
29
|
-
export declare const MARKET_TYPES: readonly ["spot", "margin", "futures"];
|
|
30
|
-
export type MarketType = (typeof MARKET_TYPES)[number];
|
|
31
|
-
export declare const ORDER_SIDES: readonly ["buy", "sell"];
|
|
32
|
-
export type OrderSide = (typeof ORDER_SIDES)[number];
|
|
33
|
-
/**
|
|
34
|
-
* Order types every venue we support implements.
|
|
35
|
-
*
|
|
36
|
-
* Deliberately short. A venue-specific type — trailing stops, iceberg, OCO —
|
|
37
|
-
* belongs behind the adapter, because supporting it here means every other
|
|
38
|
-
* adapter has to reject it and the rejection is where the bugs live.
|
|
39
|
-
*/
|
|
40
|
-
export declare const ORDER_TYPES: readonly ["market", "limit", "stop-limit"];
|
|
41
|
-
export type OrderType = (typeof ORDER_TYPES)[number];
|
|
42
|
-
/**
|
|
43
|
-
* How long an order lives.
|
|
44
|
-
*
|
|
45
|
-
* gtc rests until filled or cancelled
|
|
46
|
-
* ioc fills what it can immediately, cancels the rest
|
|
47
|
-
* fok fills entirely and immediately, or not at all
|
|
48
|
-
*
|
|
49
|
-
* `fok` matters for anything that must not partially fill — a hedge leg that
|
|
50
|
-
* half-executes leaves a position nobody asked for.
|
|
51
|
-
*/
|
|
52
|
-
export declare const TIME_IN_FORCE: readonly ["gtc", "ioc", "fok"];
|
|
53
|
-
export type TimeInForce = (typeof TIME_IN_FORCE)[number];
|
|
54
|
-
export interface VenueSpec {
|
|
55
|
-
key: string;
|
|
56
|
-
label: string;
|
|
57
|
-
/** Market types this venue offers at all. */
|
|
58
|
-
markets: readonly MarketType[];
|
|
59
|
-
/**
|
|
60
|
-
* Requests per minute the venue permits, and how it counts them.
|
|
61
|
-
*
|
|
62
|
-
* `weight` means a request costs more than one — Binance charges by response
|
|
63
|
-
* size, so a naive per-request limiter is wrong by an order of magnitude on
|
|
64
|
-
* the endpoints that matter.
|
|
65
|
-
*/
|
|
66
|
-
rateLimit: {
|
|
67
|
-
perMinute: number;
|
|
68
|
-
counts: 'requests' | 'weight';
|
|
69
|
-
};
|
|
70
|
-
/** Whether the venue supports a caller-supplied idempotency key on orders. */
|
|
71
|
-
clientOrderIds: boolean;
|
|
72
|
-
/** One clause: what a caller most needs to know before integrating. */
|
|
73
|
-
note: string;
|
|
74
|
-
}
|
|
75
|
-
export interface MarketSpec {
|
|
76
|
-
venue: string;
|
|
77
|
-
type: MarketType;
|
|
78
|
-
/** Canonical, venue-independent: `BTC/USDT`. Never a venue's own spelling. */
|
|
79
|
-
symbol: string;
|
|
80
|
-
base: string;
|
|
81
|
-
quote: string;
|
|
82
|
-
/** Quantity must be a multiple of this. */
|
|
83
|
-
lotStep: string;
|
|
84
|
-
/** Price must be a multiple of this. */
|
|
85
|
-
tickStep: string;
|
|
86
|
-
/** Smallest order value, in the quote asset. */
|
|
87
|
-
minNotional: string;
|
|
88
|
-
/** Margin and futures only. Absent on spot, and that absence is the rule. */
|
|
89
|
-
maxLeverage?: number;
|
|
90
|
-
/** Futures only. */
|
|
91
|
-
contract?: 'perpetual' | 'quarterly';
|
|
92
|
-
}
|
|
93
|
-
export declare const VENUES: readonly VenueSpec[];
|
|
94
|
-
export declare const venue: (key: string) => VenueSpec | undefined;
|
|
95
|
-
export declare const venuesFor: (type: MarketType) => readonly VenueSpec[];
|
|
96
|
-
/**
|
|
97
|
-
* `BTC/USDT` — one spelling, ours.
|
|
98
|
-
*
|
|
99
|
-
* Venues disagree: `BTCUSDT`, `BTC-USDT`, `btc_usdt`. Storing whichever the
|
|
100
|
-
* venue happened to use means a subscription created against one spelling does
|
|
101
|
-
* not match an order placed with another, and the mismatch is invisible until
|
|
102
|
-
* it is a missing position. Normalise on the way in, denormalise in the adapter.
|
|
103
|
-
*/
|
|
104
|
-
export declare const symbolOf: (base: string, quote: string) => string;
|
|
105
|
-
export declare function parseSymbol(symbol: string): {
|
|
106
|
-
base: string;
|
|
107
|
-
quote: string;
|
|
108
|
-
};
|
|
109
|
-
export declare class VenueError extends Error {
|
|
110
|
-
readonly code: 'UNKNOWN_VENUE' | 'UNKNOWN_MARKET' | 'MARKET_UNSUPPORTED' | 'BAD_SYMBOL' | 'LOT_STEP' | 'TICK_STEP' | 'MIN_NOTIONAL' | 'PRICE_REQUIRED' | 'LEVERAGE_UNSUPPORTED' | 'LEVERAGE_TOO_HIGH';
|
|
111
|
-
constructor(code: 'UNKNOWN_VENUE' | 'UNKNOWN_MARKET' | 'MARKET_UNSUPPORTED' | 'BAD_SYMBOL' | 'LOT_STEP' | 'TICK_STEP' | 'MIN_NOTIONAL' | 'PRICE_REQUIRED' | 'LEVERAGE_UNSUPPORTED' | 'LEVERAGE_TOO_HIGH', message: string);
|
|
112
|
-
}
|
|
113
|
-
export interface OrderRequest {
|
|
114
|
-
venue: string;
|
|
115
|
-
type: MarketType;
|
|
116
|
-
symbol: string;
|
|
117
|
-
side: OrderSide;
|
|
118
|
-
orderType: OrderType;
|
|
119
|
-
/** In the BASE asset. */
|
|
120
|
-
quantity: Money;
|
|
121
|
-
/** In the QUOTE asset. Required for anything that is not a market order. */
|
|
122
|
-
price?: Money;
|
|
123
|
-
stopPrice?: Money;
|
|
124
|
-
timeInForce?: TimeInForce;
|
|
125
|
-
/** Margin and futures only. */
|
|
126
|
-
leverage?: number;
|
|
127
|
-
/**
|
|
128
|
-
* The caller's own idempotency key.
|
|
129
|
-
*
|
|
130
|
-
* Not optional in practice for anything automated: without it a retried
|
|
131
|
-
* submission after a timeout places a second order, and the operator sees one
|
|
132
|
-
* confirmation for two positions.
|
|
133
|
-
*/
|
|
134
|
-
clientOrderId?: string;
|
|
135
|
-
/** Test the order against the venue without it resting. */
|
|
136
|
-
dryRun?: boolean;
|
|
137
|
-
}
|
|
138
|
-
export type OrderStatus = 'accepted' | 'partial' | 'filled' | 'cancelled' | 'rejected';
|
|
139
|
-
export interface OrderResult {
|
|
140
|
-
venueOrderId: string;
|
|
141
|
-
clientOrderId?: string;
|
|
142
|
-
status: OrderStatus;
|
|
143
|
-
filledQuantity: Money;
|
|
144
|
-
averagePrice?: Money;
|
|
145
|
-
raw?: unknown;
|
|
146
|
-
}
|
|
147
|
-
/** Notional value of an order, in the quote asset. */
|
|
148
|
-
export declare function notionalOf(request: OrderRequest, market: MarketSpec): Money;
|
|
149
|
-
/**
|
|
150
|
-
* Everything a venue would reject the order for, checked before it is sent.
|
|
151
|
-
*
|
|
152
|
-
* Locally rather than by submitting and reading the error, for three reasons: a
|
|
153
|
-
* round trip costs rate budget the caller may not have, the venue's error codes
|
|
154
|
-
* are inconsistent between market types, and a rejection after submission has
|
|
155
|
-
* already consumed an idempotency key.
|
|
156
|
-
*/
|
|
157
|
-
export declare function validateOrder(request: OrderRequest, market: MarketSpec): void;
|
|
158
|
-
/**
|
|
159
|
-
* What a venue integration provides.
|
|
160
|
-
*
|
|
161
|
-
* Deliberately small. Everything that can be decided without the venue —
|
|
162
|
-
* validation, symbol spelling, step rounding — is decided above, so an adapter
|
|
163
|
-
* is transport plus translation and nothing else. That is what keeps a second
|
|
164
|
-
* venue cheap.
|
|
165
|
-
*/
|
|
166
|
-
export interface VenueAdapter {
|
|
167
|
-
readonly venue: string;
|
|
168
|
-
/** The venue's own spelling of a canonical symbol. */
|
|
169
|
-
symbolFor(symbol: string, type: MarketType): string;
|
|
170
|
-
markets(type: MarketType): Promise<MarketSpec[]>;
|
|
171
|
-
placeOrder(request: OrderRequest, market: MarketSpec): Promise<OrderResult>;
|
|
172
|
-
cancelOrder(args: {
|
|
173
|
-
venueOrderId: string;
|
|
174
|
-
symbol: string;
|
|
175
|
-
type: MarketType;
|
|
176
|
-
}): Promise<void>;
|
|
177
|
-
openOrders(args: {
|
|
178
|
-
symbol?: string;
|
|
179
|
-
type: MarketType;
|
|
180
|
-
}): Promise<OrderResult[]>;
|
|
181
|
-
balances(type: MarketType): Promise<Money[]>;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Validate, then place.
|
|
185
|
-
*
|
|
186
|
-
* The one function a caller should use. Validating inside the adapter would
|
|
187
|
-
* make it every adapter's job, and the third one would get it wrong.
|
|
188
|
-
*/
|
|
189
|
-
export declare function submitOrder(adapter: VenueAdapter, request: OrderRequest, market: MarketSpec): Promise<OrderResult>;
|
|
190
|
-
export declare const VERSION = "0.1.0";
|