@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/dist/client.d.ts +32 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +344 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/http.d.ts +17 -0
  8. package/dist/http.d.ts.map +1 -0
  9. package/dist/http.js +106 -0
  10. package/dist/http.js.map +1 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +26 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/info-types.d.ts +380 -0
  16. package/dist/info-types.d.ts.map +1 -0
  17. package/dist/info-types.js +16 -0
  18. package/dist/info-types.js.map +1 -0
  19. package/dist/info.d.ts +65 -0
  20. package/dist/info.d.ts.map +1 -0
  21. package/dist/info.js +252 -0
  22. package/dist/info.js.map +1 -0
  23. package/dist/native.d.ts +10 -0
  24. package/dist/native.d.ts.map +1 -0
  25. package/dist/native.js +252 -0
  26. package/dist/native.js.map +1 -0
  27. package/dist/types.d.ts +143 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +13 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/wasm.d.ts +28 -0
  32. package/dist/wasm.d.ts.map +1 -0
  33. package/dist/wasm.js +279 -0
  34. package/dist/wasm.js.map +1 -0
  35. package/dist/ws.d.ts +43 -0
  36. package/dist/ws.d.ts.map +1 -0
  37. package/dist/ws.js +221 -0
  38. package/dist/ws.js.map +1 -0
  39. package/package.json +65 -0
  40. package/src/client.ts +454 -0
  41. package/src/http.ts +153 -0
  42. package/src/index.ts +144 -0
  43. package/src/info-types.ts +783 -0
  44. package/src/info.ts +355 -0
  45. package/src/native.ts +307 -0
  46. package/src/types.ts +305 -0
  47. package/src/wasm.ts +384 -0
  48. 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
+ }