@metaflux-dex/client 0.0.1
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 +157 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +344 -0
- package/dist/client.js.map +1 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +106 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/info-types.d.ts +380 -0
- package/dist/info-types.d.ts.map +1 -0
- package/dist/info-types.js +16 -0
- package/dist/info-types.js.map +1 -0
- package/dist/info.d.ts +65 -0
- package/dist/info.d.ts.map +1 -0
- package/dist/info.js +252 -0
- package/dist/info.js.map +1 -0
- package/dist/native.d.ts +10 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +252 -0
- package/dist/native.js.map +1 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm.d.ts +28 -0
- package/dist/wasm.d.ts.map +1 -0
- package/dist/wasm.js +279 -0
- package/dist/wasm.js.map +1 -0
- package/dist/ws.d.ts +43 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +221 -0
- package/dist/ws.js.map +1 -0
- package/package.json +65 -0
- package/src/client.ts +454 -0
- package/src/http.ts +153 -0
- package/src/index.ts +144 -0
- package/src/info-types.ts +783 -0
- package/src/info.ts +355 -0
- package/src/native.ts +307 -0
- package/src/types.ts +305 -0
- package/src/wasm.ts +384 -0
- package/src/ws.ts +279 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Type definitions for the @metaflux-dex/client surface.
|
|
2
|
+
//
|
|
3
|
+
// Shapes mirror the CCXT-compat REST responses emitted by the MetaFlux
|
|
4
|
+
// api-gateway (`metaflux/crates/api-gateway/src/ccxt/types.rs`). The
|
|
5
|
+
// monetary-fields-as-decimal-strings convention is load-bearing — CCXT
|
|
6
|
+
// clients pass these straight into `Decimal(value)` and any drift to
|
|
7
|
+
// number-typed fields silently loses precision.
|
|
8
|
+
//
|
|
9
|
+
// `Order` / `SignedOrder` are MTF-native (they carry pre-signed bytes
|
|
10
|
+
// the gateway forwards to the node). The other types match what the
|
|
11
|
+
// gateway emits.
|
|
12
|
+
|
|
13
|
+
/// Side as MTF wire bytes — matches `core_state::primitives::Side`.
|
|
14
|
+
/// 0 = Bid (buy), 1 = Ask (sell). The CCXT REST surface uses the
|
|
15
|
+
/// strings `"buy"` / `"sell"` instead; the Client class translates
|
|
16
|
+
/// between the two at the boundary.
|
|
17
|
+
export type Side = 0 | 1;
|
|
18
|
+
|
|
19
|
+
/// Time-in-force on the MTF wire — matches `core_state::primitives::Tif`.
|
|
20
|
+
/// 0 = GTC (Good-Til-Cancelled), 1 = IOC, 2 = ALO (Add-Liquidity-Only / post-only).
|
|
21
|
+
/// Other values are reserved.
|
|
22
|
+
export type Tif = 0 | 1 | 2;
|
|
23
|
+
|
|
24
|
+
/// Self-trade-prevention mode as the MTF wire variant index — matches the
|
|
25
|
+
/// declaration order of `core_state::primitives::StpMode`. The node decodes
|
|
26
|
+
/// the integer into the enum variant; the index ordering is load-bearing.
|
|
27
|
+
/// 0 = CancelNewest (default), 1 = CancelOldest, 2 = CancelBoth,
|
|
28
|
+
/// 3 = DecrementAndCancel.
|
|
29
|
+
export type StpMode = 0 | 1 | 2 | 3;
|
|
30
|
+
|
|
31
|
+
/// Builder-code carve attached to an order (ADR-012 §L.5.2; mirrors
|
|
32
|
+
/// `core_state::actions::trading::Builder`). When present it is encoded
|
|
33
|
+
/// INSIDE the signed order body (see `encodeLimitOrder` / the WASM
|
|
34
|
+
/// `encode_limit_order`), so the carve cannot be tampered post-signature.
|
|
35
|
+
export interface Builder {
|
|
36
|
+
/// Builder fee rate in basis points (≤ 8, and ≤ the trader's approved
|
|
37
|
+
/// per-builder ceiling — the node rejects over-cap / unapproved
|
|
38
|
+
/// builders pre-trade). Charged as an ADDITIONAL fee on the taker.
|
|
39
|
+
fee: number;
|
|
40
|
+
/// Builder address credited per fill — `0x`-prefixed 40-char hex
|
|
41
|
+
/// (20 bytes). The node rejects the zero address.
|
|
42
|
+
user: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Pre-signing order parameters. The TS-side amounts use `bigint` rather
|
|
46
|
+
/// than `number` because every monetary value on the MetaFlux wire is a
|
|
47
|
+
/// 128-bit fixed-point integer scaled by 1e8. `number` would silently
|
|
48
|
+
/// truncate values above 2^53; `bigint` matches the wire faithfully.
|
|
49
|
+
export interface Order {
|
|
50
|
+
/// On-chain asset ID (matches `core_state::primitives::AssetId(pub u32)`).
|
|
51
|
+
asset: number;
|
|
52
|
+
/// 0 = buy, 1 = sell.
|
|
53
|
+
side: Side;
|
|
54
|
+
/// Limit price, fixed-point scaled by 1e8 (matches FixedPrice on the node).
|
|
55
|
+
/// Example: 3000.50 USDC -> 300_050_000_000n.
|
|
56
|
+
priceE8: bigint;
|
|
57
|
+
/// Order size in base currency, fixed-point scaled by 1e8 (FixedSize).
|
|
58
|
+
/// Example: 0.5 BTC -> 50_000_000n.
|
|
59
|
+
sizeE8: bigint;
|
|
60
|
+
/// Time-in-force on the MTF wire.
|
|
61
|
+
tif: Tif;
|
|
62
|
+
/// Self-trade-prevention mode (matches `OrderParams.stp: StpMode`). The node
|
|
63
|
+
/// requires this field on the signed wire (no serde default). Omit to default
|
|
64
|
+
/// to `0` (CancelNewest) — the encoder fills it in.
|
|
65
|
+
stp?: StpMode;
|
|
66
|
+
/// Optional client order id (matches `OrderParams.cloid: Option<Cloid>`,
|
|
67
|
+
/// `Cloid(u128)`). On the SIGNED wire this is the raw 128-bit integer, so it
|
|
68
|
+
/// is a `bigint` here (NOT a hex string). Omit for no cloid — the encoder
|
|
69
|
+
/// skips the key and the node fills `None`.
|
|
70
|
+
cloid?: bigint;
|
|
71
|
+
/// Reduce-only flag (matches `OrderParams.reduce_only: bool`). The node
|
|
72
|
+
/// requires this field on the signed wire (no serde default). Omit to default
|
|
73
|
+
/// to `false` — the encoder fills it in.
|
|
74
|
+
reduceOnly?: boolean;
|
|
75
|
+
/// Optional builder-code carve (ADR-012 §L.5.2). Omit for a vanilla
|
|
76
|
+
/// order; when set it rides inside the EIP-712-signed body.
|
|
77
|
+
builder?: Builder;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Order body + signature bundle the client posts to the gateway.
|
|
81
|
+
///
|
|
82
|
+
/// `payload` is the rmp_serde-encoded body produced by
|
|
83
|
+
/// `encode_limit_order` in the WASM crate. `signature` is the 65-byte
|
|
84
|
+
/// `r || s || v` recoverable ECDSA sig over the EIP-712 typed-data
|
|
85
|
+
/// hash of `payload`. `signer` is the 20-byte EVM address derived from
|
|
86
|
+
/// the signing key — included so the gateway can reject envelopes
|
|
87
|
+
/// whose recovered key doesn't match the claimed sender without first
|
|
88
|
+
/// doing the ECDSA recovery (cheap rejection of obvious replays).
|
|
89
|
+
export interface SignedOrder {
|
|
90
|
+
/// MTF-canonical wire bytes of the unsigned order body.
|
|
91
|
+
payload: Uint8Array;
|
|
92
|
+
/// 65-byte recoverable secp256k1 signature: `r || s || v`.
|
|
93
|
+
signature: Uint8Array;
|
|
94
|
+
/// 20-byte EVM address the signature claims to be from.
|
|
95
|
+
signer: Uint8Array;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// MTF-native order action shape (snake_case), byte-for-byte mirror of the
|
|
99
|
+
/// server `NativeOrder` (`metaflux/crates/api-node/src/rest/native_action.rs`).
|
|
100
|
+
/// These string/number forms are EXACTLY what rides inside the signed
|
|
101
|
+
/// `action` JSON posted to `POST /exchange` — the digest covers the
|
|
102
|
+
/// full object, so every field here is part of the signed bytes.
|
|
103
|
+
///
|
|
104
|
+
/// Field ORDER is load-bearing: the server verifies the signature over the
|
|
105
|
+
/// raw `action` bytes, so the client must emit keys in this exact order and
|
|
106
|
+
/// the same bytes it signed (see `buildNativeOrderAction`).
|
|
107
|
+
export interface NativeOrder {
|
|
108
|
+
/// `0x`-hex 20-byte owner. MUST equal the signing wallet's address; the
|
|
109
|
+
/// server authenticates via the recovered signer and requires it to equal
|
|
110
|
+
/// `owner` (or an approved agent of it).
|
|
111
|
+
owner: string;
|
|
112
|
+
/// Target market id (`u32`).
|
|
113
|
+
market: number;
|
|
114
|
+
/// Side: `"bid"` (buy) or `"ask"` (sell).
|
|
115
|
+
side: NativeSide;
|
|
116
|
+
/// Order kind. Only `"limit"` / `"market"` map server-side today.
|
|
117
|
+
kind: NativeOrderKind;
|
|
118
|
+
/// Size in fixed-point tick units (`u64` on the wire).
|
|
119
|
+
size: number;
|
|
120
|
+
/// Limit price in fixed-point tick units (`u64` on the wire).
|
|
121
|
+
limit_px: number;
|
|
122
|
+
/// Time-in-force.
|
|
123
|
+
tif: NativeTif;
|
|
124
|
+
/// Self-trade-prevention mode.
|
|
125
|
+
stp_mode: NativeStpMode;
|
|
126
|
+
/// Reduce-only flag.
|
|
127
|
+
reduce_only: boolean;
|
|
128
|
+
/// Optional `0x`-hex 32-char (16-byte) client order id. Omitted from the
|
|
129
|
+
/// signed bytes entirely when absent.
|
|
130
|
+
cloid?: string;
|
|
131
|
+
/// Optional builder-code carve. Rides INSIDE the signed action object.
|
|
132
|
+
builder?: NativeBuilder;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// MTF-native `cancel_order` action shape (snake_case), byte-for-byte mirror of
|
|
136
|
+
/// the server `NativeCancel` (`metaflux/crates/api-node/src/rest/native_action.rs`).
|
|
137
|
+
/// Field ORDER is load-bearing for the same reason as `NativeOrder`: the server
|
|
138
|
+
/// verifies the signature over the raw `action` bytes (see
|
|
139
|
+
/// `buildNativeCancelAction`).
|
|
140
|
+
export interface NativeCancel {
|
|
141
|
+
/// `0x`-hex 20-byte owner. MUST equal the signing wallet's address.
|
|
142
|
+
owner: string;
|
|
143
|
+
/// Target market id (`u32`).
|
|
144
|
+
market: number;
|
|
145
|
+
/// Server order id (`u64`). REQUIRED for the cancel to lower server-side —
|
|
146
|
+
/// the core `CancelParams` cancels by `oid`. Omit only if cancelling by
|
|
147
|
+
/// `cloid` (currently rejected at lowering, but accepted on the wire).
|
|
148
|
+
oid?: number;
|
|
149
|
+
/// Optional `0x`-hex 32-char (16-byte) client order id. Omitted from the
|
|
150
|
+
/// signed bytes entirely when absent.
|
|
151
|
+
cloid?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// MTF-native side string — mirrors the server `NativeSide`.
|
|
155
|
+
export type NativeSide = 'bid' | 'ask';
|
|
156
|
+
|
|
157
|
+
/// MTF-native order kind — mirrors the server `NativeOrderKind`. Only
|
|
158
|
+
/// `limit` / `market` are mapped server-side; `stop_loss` / `take_profit`
|
|
159
|
+
/// are rejected (triggers not wired).
|
|
160
|
+
export type NativeOrderKind = 'limit' | 'market' | 'stop_loss' | 'take_profit';
|
|
161
|
+
|
|
162
|
+
/// MTF-native time-in-force — mirrors the server `NativeTif`. `aon` is
|
|
163
|
+
/// rejected server-side (no core equivalent).
|
|
164
|
+
export type NativeTif = 'gtc' | 'ioc' | 'aon' | 'alo';
|
|
165
|
+
|
|
166
|
+
/// MTF-native self-trade-prevention — mirrors the server `NativeStpMode`.
|
|
167
|
+
/// `reject` is rejected server-side (no core equivalent).
|
|
168
|
+
export type NativeStpMode =
|
|
169
|
+
| 'cancel_oldest'
|
|
170
|
+
| 'cancel_newest'
|
|
171
|
+
| 'cancel_both'
|
|
172
|
+
| 'reject';
|
|
173
|
+
|
|
174
|
+
/// MTF-native builder carve — mirrors the server `NativeBuilder`. Rides
|
|
175
|
+
/// inside the signed action bytes.
|
|
176
|
+
export interface NativeBuilder {
|
|
177
|
+
/// Builder fee in basis points (`u16`).
|
|
178
|
+
fee: number;
|
|
179
|
+
/// `0x`-hex 20-byte address credited with the builder fee.
|
|
180
|
+
user: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Signed native action envelope posted to `POST /exchange`.
|
|
184
|
+
///
|
|
185
|
+
/// `action` is the raw JSON STRING (not a parsed object) so the bytes sent
|
|
186
|
+
/// are byte-identical to the bytes signed — the server recovers the signer
|
|
187
|
+
/// over the exact `action` bytes (`serde_json::value::RawValue`). `signature`
|
|
188
|
+
/// is the `0x`-prefixed 65-byte `r||s||v` secp256k1 signature.
|
|
189
|
+
export interface NativeSignedAction {
|
|
190
|
+
/// Raw JSON bytes of the action object — what was signed AND what is sent.
|
|
191
|
+
actionJson: string;
|
|
192
|
+
/// Per-owner replay nonce bound into the signed digest.
|
|
193
|
+
nonce: bigint;
|
|
194
|
+
/// `0x`-prefixed 65-byte recoverable secp256k1 signature.
|
|
195
|
+
signature: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Per-order status entry returned by `/exchange` for an order-type action.
|
|
199
|
+
///
|
|
200
|
+
/// Byte-for-byte the node `OrderStatusEntry`
|
|
201
|
+
/// (`metaflux/crates/api-node/src/rest/exchange.rs`): a tagged union selected by
|
|
202
|
+
/// the single present key, one entry per submitted order, in submission order.
|
|
203
|
+
/// `total_sz` / `avg_px` are 8-decimal fixed-point u128 STRINGS (native JSON
|
|
204
|
+
/// numbers lose precision past 2^53); `oid` / `nonce` are JSON numbers.
|
|
205
|
+
export type OrderStatus =
|
|
206
|
+
/// Posted to the book; not (fully) filled. `cloid` echoed only when supplied.
|
|
207
|
+
| { resting: { oid: number; cloid?: string } }
|
|
208
|
+
/// Crossed for `total_sz` at `avg_px`.
|
|
209
|
+
| { filled: { oid: number; total_sz: string; avg_px: string } }
|
|
210
|
+
/// This entry was rejected at admission/commit (the rest of a batch may still
|
|
211
|
+
/// have succeeded).
|
|
212
|
+
| { error: string }
|
|
213
|
+
/// Admitted, but no commit observed within the wait window — track via
|
|
214
|
+
/// `/info` / WS. NOT a fabricated oid.
|
|
215
|
+
| { pending: { action_hash: string; nonce: number } };
|
|
216
|
+
|
|
217
|
+
/// Server response to `POST /exchange`. Mirrors the node `ExchangeResponse`
|
|
218
|
+
/// (`metaflux/crates/api-node/src/rest/exchange.rs`).
|
|
219
|
+
///
|
|
220
|
+
/// Two shapes share this struct (the node omits the absent keys):
|
|
221
|
+
/// - **Order-type actions** (`submit_order` / `batch_order` / `cancel_order` /
|
|
222
|
+
/// …) carry `statuses` — the per-order union array. There is NO top-level
|
|
223
|
+
/// `oid`; the order id rides inside each status entry.
|
|
224
|
+
/// - **Every other action** (and admission-time rejections) carries the
|
|
225
|
+
/// admission envelope: `accepted` + `mempool_depth` + (`nonce` / `action_hash`
|
|
226
|
+
/// on success, `error` on rejection).
|
|
227
|
+
export interface NativeExchangeAck {
|
|
228
|
+
/// Per-order status union — present only for order-type actions.
|
|
229
|
+
statuses?: OrderStatus[];
|
|
230
|
+
/// Whether the action was admitted to the mempool (admission envelope; omitted
|
|
231
|
+
/// on the order path).
|
|
232
|
+
accepted?: boolean;
|
|
233
|
+
/// Rejection reason, when `accepted` is false.
|
|
234
|
+
error?: string;
|
|
235
|
+
/// Mempool depth observed at admission time (diagnostic; admission envelope).
|
|
236
|
+
mempool_depth?: number;
|
|
237
|
+
/// Echoed replay nonce (admission envelope).
|
|
238
|
+
nonce?: number;
|
|
239
|
+
/// Deterministic action identifier — `0x` + keccak256 of the action bytes;
|
|
240
|
+
/// matches against commit events (admission envelope).
|
|
241
|
+
action_hash?: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Acknowledgement from `submitOrder`. Mirrors `Order` from the CCXT REST
|
|
245
|
+
/// response shape (`api-gateway/src/ccxt/types.rs::Order`); monetary
|
|
246
|
+
/// fields are decimal strings to match what the gateway emits.
|
|
247
|
+
export interface OrderAck {
|
|
248
|
+
id: string;
|
|
249
|
+
timestamp: number;
|
|
250
|
+
datetime: string;
|
|
251
|
+
symbol: string;
|
|
252
|
+
type: 'limit' | 'market';
|
|
253
|
+
side: 'buy' | 'sell';
|
|
254
|
+
price?: string;
|
|
255
|
+
amount: string;
|
|
256
|
+
filled: string;
|
|
257
|
+
remaining: string;
|
|
258
|
+
status: 'open' | 'closed' | 'canceled' | 'expired' | 'rejected';
|
|
259
|
+
clientOrderId?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// CCXT market descriptor — one entry of `fetchMarkets`. See the
|
|
263
|
+
/// gateway's `Market` struct for the exhaustive shape; we model the
|
|
264
|
+
/// fields every CCXT client iterates and leave room for extension.
|
|
265
|
+
export interface Market {
|
|
266
|
+
id: string;
|
|
267
|
+
symbol: string;
|
|
268
|
+
base: string;
|
|
269
|
+
quote: string;
|
|
270
|
+
settle?: string;
|
|
271
|
+
active: boolean;
|
|
272
|
+
spot: boolean;
|
|
273
|
+
contract: boolean;
|
|
274
|
+
swap: boolean;
|
|
275
|
+
future: boolean;
|
|
276
|
+
linear: boolean;
|
|
277
|
+
precision: { amount: number; price: number };
|
|
278
|
+
limits: {
|
|
279
|
+
amount: { min?: string; max?: string };
|
|
280
|
+
price: { min?: string; max?: string };
|
|
281
|
+
cost: { min?: string; max?: string };
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// CCXT position snapshot. Sign of the position is in `side`, never in
|
|
286
|
+
/// `contracts` (always non-negative).
|
|
287
|
+
export interface Position {
|
|
288
|
+
symbol: string;
|
|
289
|
+
side: 'long' | 'short';
|
|
290
|
+
contracts: string;
|
|
291
|
+
notional?: string;
|
|
292
|
+
entryPrice?: string;
|
|
293
|
+
markPrice?: string;
|
|
294
|
+
liquidationPrice?: string;
|
|
295
|
+
unrealizedPnl?: string;
|
|
296
|
+
leverage?: string;
|
|
297
|
+
timestamp: number;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// Error envelope every CCXT 4xx/5xx response carries. The Client
|
|
301
|
+
/// throws `MetaFluxApiError` (defined in http.ts) when the gateway
|
|
302
|
+
/// returns this shape.
|
|
303
|
+
export interface ErrorEnvelope {
|
|
304
|
+
error: string;
|
|
305
|
+
}
|
package/src/wasm.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// WASM module loader + typed facade.
|
|
2
|
+
//
|
|
3
|
+
// The wasm-pack-emitted `pkg/metaflux_client_wasm.js` is dynamically
|
|
4
|
+
// imported so the bundle can be tree-shaken when consumers only want
|
|
5
|
+
// the pure-TS REST surface. `loadWasm()` is idempotent; first call
|
|
6
|
+
// instantiates, subsequent calls reuse the resolved promise.
|
|
7
|
+
//
|
|
8
|
+
// Why dynamic import + facade?
|
|
9
|
+
//
|
|
10
|
+
// 1. The `pkg/` directory is only present after `npm run build:wasm`.
|
|
11
|
+
// A top-level `import` would crash on a fresh clone before that step
|
|
12
|
+
// runs; dynamic import lets the WASM-using code paths fail with a
|
|
13
|
+
// helpful error pointing to the build command.
|
|
14
|
+
// 2. The wasm-bindgen-generated module exports raw `Uint8Array` returns
|
|
15
|
+
// that are zero-length on encoder failure (see `lib.rs`). The facade
|
|
16
|
+
// promotes that to a typed exception, keeping the Client surface
|
|
17
|
+
// free of "did this succeed?" branches.
|
|
18
|
+
|
|
19
|
+
import type { Builder, Side, StpMode, Tif } from './types.js';
|
|
20
|
+
|
|
21
|
+
/// Shape of the WASM module after `pkg/` is built. Mirrors the
|
|
22
|
+
/// `#[wasm_bindgen]` exports in `wasm/src/lib.rs`. Kept narrow — we only
|
|
23
|
+
/// document the symbols the TS layer calls.
|
|
24
|
+
interface WasmModule {
|
|
25
|
+
default: (path?: string | URL | Request) => Promise<unknown>;
|
|
26
|
+
keccak256: (data: Uint8Array) => Uint8Array;
|
|
27
|
+
sign_secp256k1: (privKey: Uint8Array, messageHash: Uint8Array) => Uint8Array;
|
|
28
|
+
recover_pubkey: (sig: Uint8Array, messageHash: Uint8Array) => Uint8Array;
|
|
29
|
+
verify_secp256k1: (
|
|
30
|
+
pubkeyCompressed: Uint8Array,
|
|
31
|
+
sig: Uint8Array,
|
|
32
|
+
messageHash: Uint8Array,
|
|
33
|
+
) => boolean;
|
|
34
|
+
eip712_typed_data_hash: (
|
|
35
|
+
domainSeparator: Uint8Array,
|
|
36
|
+
messageHash: Uint8Array,
|
|
37
|
+
) => Uint8Array;
|
|
38
|
+
encode_limit_order: (
|
|
39
|
+
asset: number,
|
|
40
|
+
side: number,
|
|
41
|
+
sizeE8Lo: bigint,
|
|
42
|
+
sizeE8Hi: bigint,
|
|
43
|
+
priceE8Lo: bigint,
|
|
44
|
+
priceE8Hi: bigint,
|
|
45
|
+
tif: number,
|
|
46
|
+
stp: number,
|
|
47
|
+
hasCloid: boolean,
|
|
48
|
+
cloidLo: bigint,
|
|
49
|
+
cloidHi: bigint,
|
|
50
|
+
reduceOnly: boolean,
|
|
51
|
+
hasBuilder: boolean,
|
|
52
|
+
builderFee: number,
|
|
53
|
+
builderUser: Uint8Array,
|
|
54
|
+
) => Uint8Array;
|
|
55
|
+
derive_address_from_pubkey: (pubkey: Uint8Array) => Uint8Array;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let wasmPromise: Promise<WasmModule> | undefined;
|
|
59
|
+
|
|
60
|
+
/// Custom error thrown when the WASM module cannot be loaded — e.g. on
|
|
61
|
+
/// a fresh clone before `npm run build:wasm` has produced `pkg/`. The
|
|
62
|
+
/// message points at the fix.
|
|
63
|
+
export class WasmNotBuiltError extends Error {
|
|
64
|
+
constructor(cause?: unknown) {
|
|
65
|
+
super(
|
|
66
|
+
'metaflux_client_wasm not built. Run `npm run build:wasm` first ' +
|
|
67
|
+
'(requires `wasm-pack`; install via `brew install wasm-pack`).',
|
|
68
|
+
);
|
|
69
|
+
this.name = 'WasmNotBuiltError';
|
|
70
|
+
if (cause !== undefined) {
|
|
71
|
+
(this as { cause?: unknown }).cause = cause;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Thrown when a WASM-side encoder/signer returns an empty buffer (the
|
|
77
|
+
/// `lib.rs` convention for invalid input). Carries the failed call name.
|
|
78
|
+
export class WasmCallError extends Error {
|
|
79
|
+
constructor(public readonly call: string, hint?: string) {
|
|
80
|
+
super(
|
|
81
|
+
`WASM call '${call}' returned empty result` +
|
|
82
|
+
(hint !== undefined ? ` (${hint})` : ''),
|
|
83
|
+
);
|
|
84
|
+
this.name = 'WasmCallError';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Idempotently load + initialise the WASM module.
|
|
89
|
+
///
|
|
90
|
+
/// First call dynamically imports `../pkg/metaflux_client_wasm.js`
|
|
91
|
+
/// (the wasm-pack output), invokes the default-export `init`, and
|
|
92
|
+
/// caches the resolved module. Subsequent calls return the cached
|
|
93
|
+
/// promise — both fast-path and re-entrant safe.
|
|
94
|
+
///
|
|
95
|
+
/// The dynamic import path is built from a string variable so the
|
|
96
|
+
/// TypeScript compiler does not try to statically resolve the
|
|
97
|
+
/// (until-built) `pkg/` directory at typecheck time. The shape
|
|
98
|
+
/// expected from the loaded module is enforced by the cast to
|
|
99
|
+
/// `WasmModule`.
|
|
100
|
+
export async function loadWasm(): Promise<WasmModule> {
|
|
101
|
+
if (wasmPromise === undefined) {
|
|
102
|
+
wasmPromise = (async () => {
|
|
103
|
+
let mod: WasmModule;
|
|
104
|
+
try {
|
|
105
|
+
// Path is relative to the *compiled* dist/ output (one level
|
|
106
|
+
// above pkg/ at the repo root). At dev-mode (vitest running
|
|
107
|
+
// .ts directly) the same relative path lands on the same
|
|
108
|
+
// pkg/ directory because vitest cwd is the repo root.
|
|
109
|
+
const wasmPath = '../pkg/metaflux_client_wasm.js';
|
|
110
|
+
mod = (await import(/* @vite-ignore */ wasmPath)) as WasmModule;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
throw new WasmNotBuiltError(err);
|
|
113
|
+
}
|
|
114
|
+
// wasm-pack's default-export `init` accepts an optional .wasm source;
|
|
115
|
+
// we let it auto-resolve relative to the .js shim. The Node-target
|
|
116
|
+
// build (`--target nodejs`) loads the .wasm synchronously at
|
|
117
|
+
// module-load time and exposes NO default export — only `web` /
|
|
118
|
+
// `bundler` builds need this call.
|
|
119
|
+
if (typeof (mod as { default?: unknown }).default === 'function') {
|
|
120
|
+
const init = (mod as { default: (arg?: unknown) => Promise<unknown> })
|
|
121
|
+
.default;
|
|
122
|
+
try {
|
|
123
|
+
// Browser / bundler: default-resolve the .wasm via fetch.
|
|
124
|
+
await init();
|
|
125
|
+
} catch (fetchErr) {
|
|
126
|
+
// Node (incl. vitest): the web-target shim's `init()` fetches the
|
|
127
|
+
// .wasm by file URL, which Node's fetch refuses. Fall back to
|
|
128
|
+
// reading + compiling the bytes ourselves and handing them to
|
|
129
|
+
// init(). Only attempt this where `node:fs` is importable; if it
|
|
130
|
+
// is not (true browser), rethrow the original fetch error.
|
|
131
|
+
const compiled = await compileWasmFromFs();
|
|
132
|
+
if (compiled === undefined) throw fetchErr;
|
|
133
|
+
await init({ module_or_path: compiled });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return mod;
|
|
137
|
+
})();
|
|
138
|
+
}
|
|
139
|
+
return wasmPromise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Reset the cached module — used by tests that want to exercise the
|
|
143
|
+
/// WasmNotBuiltError path. Not part of the public API.
|
|
144
|
+
export function _resetWasmCacheForTest(): void {
|
|
145
|
+
wasmPromise = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Node-only: read + compile the wasm-pack `_bg.wasm` bytes so the web-target
|
|
149
|
+
/// `init()` can be fed a ready module instead of fetching a file URL (which
|
|
150
|
+
/// Node's `fetch` rejects). Returns `undefined` in environments without
|
|
151
|
+
/// `node:fs` (a real browser), where the fetch path is correct anyway.
|
|
152
|
+
async function compileWasmFromFs(): Promise<WebAssembly.Module | undefined> {
|
|
153
|
+
try {
|
|
154
|
+
const fs = await import('node:fs/promises');
|
|
155
|
+
const url = await import('node:url');
|
|
156
|
+
const path = await import('node:path');
|
|
157
|
+
// Resolve the .wasm next to the wasm-pack .js shim. From dev (vitest on
|
|
158
|
+
// src/*.ts) the cwd is the repo root and pkg/ sits there; from the
|
|
159
|
+
// compiled dist/ output pkg/ is one level up. Try both.
|
|
160
|
+
const candidates = [
|
|
161
|
+
path.resolve(process.cwd(), 'pkg', 'metaflux_client_wasm_bg.wasm'),
|
|
162
|
+
url.fileURLToPath(
|
|
163
|
+
new URL('../pkg/metaflux_client_wasm_bg.wasm', import.meta.url),
|
|
164
|
+
),
|
|
165
|
+
];
|
|
166
|
+
for (const c of candidates) {
|
|
167
|
+
try {
|
|
168
|
+
const bytes = await fs.readFile(c);
|
|
169
|
+
return await WebAssembly.compile(bytes);
|
|
170
|
+
} catch {
|
|
171
|
+
// Try the next candidate.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
} catch {
|
|
176
|
+
// `node:fs` unavailable -> not Node; the fetch path is the right one.
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Typed wrappers — each promotes an empty WASM result to a thrown error.
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
/// `keccak256(data)` -> 32-byte digest. Throws `WasmCallError` on empty
|
|
186
|
+
/// result (should not happen — keccak accepts any length input).
|
|
187
|
+
export async function keccak256(data: Uint8Array): Promise<Uint8Array> {
|
|
188
|
+
const wasm = await loadWasm();
|
|
189
|
+
const out = wasm.keccak256(data);
|
|
190
|
+
if (out.length !== 32) {
|
|
191
|
+
throw new WasmCallError('keccak256', `got ${out.length} bytes, want 32`);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Produce a recoverable ECDSA signature in `r || s || v` 65-byte form.
|
|
197
|
+
/// Throws on a malformed private key or message hash length.
|
|
198
|
+
export async function signSecp256k1(
|
|
199
|
+
privKey: Uint8Array,
|
|
200
|
+
messageHash: Uint8Array,
|
|
201
|
+
): Promise<Uint8Array> {
|
|
202
|
+
const wasm = await loadWasm();
|
|
203
|
+
const out = wasm.sign_secp256k1(privKey, messageHash);
|
|
204
|
+
if (out.length !== 65) {
|
|
205
|
+
throw new WasmCallError(
|
|
206
|
+
'sign_secp256k1',
|
|
207
|
+
'invalid private key or message hash length (expected 32 bytes each)',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Recover the 33-byte compressed pubkey from a sig + message digest.
|
|
214
|
+
export async function recoverPubkey(
|
|
215
|
+
sig: Uint8Array,
|
|
216
|
+
messageHash: Uint8Array,
|
|
217
|
+
): Promise<Uint8Array> {
|
|
218
|
+
const wasm = await loadWasm();
|
|
219
|
+
const out = wasm.recover_pubkey(sig, messageHash);
|
|
220
|
+
if (out.length !== 33) {
|
|
221
|
+
throw new WasmCallError(
|
|
222
|
+
'recover_pubkey',
|
|
223
|
+
'malformed signature or message hash',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// EIP-712 envelope hash: `keccak256(0x1901 || domain || message)`.
|
|
230
|
+
export async function eip712TypedDataHash(
|
|
231
|
+
domainSeparator: Uint8Array,
|
|
232
|
+
messageHash: Uint8Array,
|
|
233
|
+
): Promise<Uint8Array> {
|
|
234
|
+
const wasm = await loadWasm();
|
|
235
|
+
const out = wasm.eip712_typed_data_hash(domainSeparator, messageHash);
|
|
236
|
+
if (out.length !== 32) {
|
|
237
|
+
throw new WasmCallError(
|
|
238
|
+
'eip712_typed_data_hash',
|
|
239
|
+
`got ${out.length} bytes, want 32`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// Encode the canonical msgpack body for an `order` action. The 128-bit
|
|
246
|
+
/// `priceE8` / `sizeE8` amounts are split into (lo, hi) `bigint` words
|
|
247
|
+
/// for the wasm-bindgen ABI — see `lib.rs::u128_from_parts`.
|
|
248
|
+
///
|
|
249
|
+
/// `stp` (self-trade-prevention) and `reduceOnly` are REQUIRED on the node's
|
|
250
|
+
/// signed wire (`OrderParams` has no serde default for either); they default
|
|
251
|
+
/// here to `0` (CancelNewest) / `false` so callers can omit them, but are
|
|
252
|
+
/// always emitted into the body.
|
|
253
|
+
///
|
|
254
|
+
/// `cloid` (optional client order id) rides the signed body as the raw 128-bit
|
|
255
|
+
/// integer — the node's `Cloid(u128)` wire form — split into (lo, hi) words for
|
|
256
|
+
/// the wasm-bindgen ABI. Omit for no cloid (the key is skipped; the node fills
|
|
257
|
+
/// `None`).
|
|
258
|
+
///
|
|
259
|
+
/// `builder` (ADR-012 §L.5.2) is optional. When supplied it is encoded
|
|
260
|
+
/// INSIDE the body so the carve is covered by the EIP-712 signature; an
|
|
261
|
+
/// omitted builder produces byte-identical output to the pre-builder
|
|
262
|
+
/// encoder (the `builder` key is skipped, and the node defaults it to
|
|
263
|
+
/// `None`).
|
|
264
|
+
export async function encodeLimitOrder(
|
|
265
|
+
asset: number,
|
|
266
|
+
side: Side,
|
|
267
|
+
sizeE8: bigint,
|
|
268
|
+
priceE8: bigint,
|
|
269
|
+
tif: Tif,
|
|
270
|
+
stp: StpMode = 0,
|
|
271
|
+
cloid?: bigint,
|
|
272
|
+
reduceOnly = false,
|
|
273
|
+
builder?: Builder,
|
|
274
|
+
): Promise<Uint8Array> {
|
|
275
|
+
if (sizeE8 < 0n) throw new RangeError('sizeE8 must be non-negative');
|
|
276
|
+
if (priceE8 < 0n) throw new RangeError('priceE8 must be non-negative');
|
|
277
|
+
if (sizeE8 >= 1n << 128n)
|
|
278
|
+
throw new RangeError('sizeE8 overflows u128');
|
|
279
|
+
if (priceE8 >= 1n << 128n)
|
|
280
|
+
throw new RangeError('priceE8 overflows u128');
|
|
281
|
+
const mask64 = (1n << 64n) - 1n;
|
|
282
|
+
const sizeLo = sizeE8 & mask64;
|
|
283
|
+
const sizeHi = (sizeE8 >> 64n) & mask64;
|
|
284
|
+
const priceLo = priceE8 & mask64;
|
|
285
|
+
const priceHi = (priceE8 >> 64n) & mask64;
|
|
286
|
+
|
|
287
|
+
// Cloid: optional 128-bit client order id. On the signed wire it is the
|
|
288
|
+
// raw u128 (node `Cloid(u128)`), split into (lo, hi) for the ABI.
|
|
289
|
+
let hasCloid = false;
|
|
290
|
+
let cloidLo = 0n;
|
|
291
|
+
let cloidHi = 0n;
|
|
292
|
+
if (cloid !== undefined) {
|
|
293
|
+
if (cloid < 0n) throw new RangeError('cloid must be non-negative');
|
|
294
|
+
if (cloid >= 1n << 128n) throw new RangeError('cloid overflows u128');
|
|
295
|
+
hasCloid = true;
|
|
296
|
+
cloidLo = cloid & mask64;
|
|
297
|
+
cloidHi = (cloid >> 64n) & mask64;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Builder carve. Validate fee + address shape on the TS side so a
|
|
301
|
+
// malformed carve fails loudly here rather than encoding silently —
|
|
302
|
+
// an unsigned or dropped builder would be worse than a thrown error.
|
|
303
|
+
let hasBuilder = false;
|
|
304
|
+
let builderFee = 0;
|
|
305
|
+
let builderUser: Uint8Array = EMPTY_BYTES;
|
|
306
|
+
if (builder !== undefined) {
|
|
307
|
+
if (!Number.isInteger(builder.fee) || builder.fee < 0 || builder.fee > 0xffff) {
|
|
308
|
+
throw new RangeError('builder.fee must be a u16 (0..=65535) in basis points');
|
|
309
|
+
}
|
|
310
|
+
builderUser = addressHexToBytes(builder.user);
|
|
311
|
+
hasBuilder = true;
|
|
312
|
+
builderFee = builder.fee;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const wasm = await loadWasm();
|
|
316
|
+
const out = wasm.encode_limit_order(
|
|
317
|
+
asset,
|
|
318
|
+
side,
|
|
319
|
+
sizeLo,
|
|
320
|
+
sizeHi,
|
|
321
|
+
priceLo,
|
|
322
|
+
priceHi,
|
|
323
|
+
tif,
|
|
324
|
+
stp,
|
|
325
|
+
hasCloid,
|
|
326
|
+
cloidLo,
|
|
327
|
+
cloidHi,
|
|
328
|
+
reduceOnly,
|
|
329
|
+
hasBuilder,
|
|
330
|
+
builderFee,
|
|
331
|
+
builderUser,
|
|
332
|
+
);
|
|
333
|
+
if (out.length === 0) {
|
|
334
|
+
throw new WasmCallError(
|
|
335
|
+
'encode_limit_order',
|
|
336
|
+
'msgpack encoder failed (invalid builder address length?)',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Shared empty buffer for the no-builder fast path — avoids allocating a
|
|
343
|
+
/// fresh zero-length `Uint8Array` per call.
|
|
344
|
+
const EMPTY_BYTES = new Uint8Array(0);
|
|
345
|
+
|
|
346
|
+
/// Parse a `0x`-prefixed (or bare) 40-char hex address into 20 raw bytes.
|
|
347
|
+
/// Mirrors `core_state::address::Address::from_hex`. Throws on any
|
|
348
|
+
/// non-hex char or wrong length — a malformed builder address must never
|
|
349
|
+
/// reach the signed body.
|
|
350
|
+
function addressHexToBytes(addr: string): Uint8Array {
|
|
351
|
+
const hex = addr.startsWith('0x') ? addr.slice(2) : addr;
|
|
352
|
+
if (hex.length !== 40) {
|
|
353
|
+
throw new RangeError(
|
|
354
|
+
`builder.user must be a 20-byte (40 hex char) address, got '${addr}'`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
// Whole-string hex check up front — `parseInt('1z', 16)` would silently
|
|
358
|
+
// return 1, so per-byte parsing alone could mask a malformed char.
|
|
359
|
+
if (!/^[0-9a-fA-F]{40}$/.test(hex)) {
|
|
360
|
+
throw new RangeError(`builder.user contains a non-hex character: '${addr}'`);
|
|
361
|
+
}
|
|
362
|
+
const out = new Uint8Array(20);
|
|
363
|
+
for (let i = 0; i < 20; i++) {
|
|
364
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Derive the standard 20-byte EVM address from a SEC1 public key.
|
|
370
|
+
/// Accepts 33-byte compressed, 64-byte raw x||y, or 65-byte
|
|
371
|
+
/// uncompressed `0x04 || x || y`.
|
|
372
|
+
export async function deriveAddressFromPubkey(
|
|
373
|
+
pubkey: Uint8Array,
|
|
374
|
+
): Promise<Uint8Array> {
|
|
375
|
+
const wasm = await loadWasm();
|
|
376
|
+
const out = wasm.derive_address_from_pubkey(pubkey);
|
|
377
|
+
if (out.length !== 20) {
|
|
378
|
+
throw new WasmCallError(
|
|
379
|
+
'derive_address_from_pubkey',
|
|
380
|
+
'malformed pubkey (expected 33, 64, or 65 bytes)',
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|