Skip to main content

TypeScript Client

TL;DR

A reference client that ties the pieces together: get a bearer token, read markets and server time, use the SDK's order builders to assemble a signed order from a deposited note, submit it, and subscribe to the order and fill streams. The SDK owns the cryptography (note commitments, the input proof, and the anchor pool) so your code works in economic terms.

What the SDK does for you

The hard part of a Darknyx order is its cryptographic backing: the collateral-note commitment, the zero-knowledge input proof, the owner-commitment opening, and the continuation anchor pool (see Place Order). The SDK derives all of it from your seed and a spendable note, and signs the canonical body with your trading key. You supply the intent (side, amount, price, time-in-force) and get back a ready-to-send order.

The SDK also ships:

  • Order builders: presets for market, all-or-none, and good-til-time orders over the native fields. They include the viewing_pubkey for on-chain change-amount recovery by default, so a change note stays recoverable without any extra work.
  • Stream clients: per-account order-lifecycle and fill subscriptions, with the fill-memo verification built in.
  • Recovery helpers: derive a deterministic master seed from a wallet signature (seedFromWalletSignature, over MASTER_SEED_MESSAGE) so the same wallet reproduces your keys on any device, and recover change notes from the on-chain ciphertext (recoverChangeFromChain) when backfilling.
  • System helpers: server time (for slot-based expiry) and the degraded-mode status.

Client implementation

import {
marketPolicy,
aonPolicy,
gttLimitPolicy,
OrderSide,
fetchServerTime,
fetchSystemStatus,
subscribeOrderUpdates,
subscribeFills,
// order submission
proveAndBuildOrder,
buildOrder,
buildCancel,
nodeValidInputProver,
placeOrder,
TradingClient,
DarknyxApiError,
deriveOrderId,
} from "@darknyx/sdk";

class DarknyxClient {
private token: string | null = null;

constructor(private gateway: string) {
this.gateway = gateway.replace(/\/$/, "");
}

// ── Auth ──────────────────────────────────────────────────────────────
async login(apiKey: string, apiSecret: string, passphrase: string) {
const r = await fetch(`${this.gateway}/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: apiKey, api_secret: apiSecret, passphrase }),
});
if (!r.ok) throw new Error(`auth ${r.status}: ${await r.text()}`);
const body = await r.json();
this.token = body.access_token;
return body; // { access_token, token_type, expires_in, account_id }
}

private auth() {
if (!this.token) throw new Error("call login() first");
return { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json" };
}

// ── Reference data + health ──────────────────────────────────────────
getInstruments() {
return fetch(`${this.gateway}/instruments`).then((r) => r.json());
}
systemStatus() {
return fetchSystemStatus(this.gateway);
}
serverTime() {
return fetchServerTime(this.gateway);
}

// ── Orders ───────────────────────────────────────────────────────────
// `order` is a fully-built, signed wire body, produced by the SDK's
// order-builder from your keys + a spendable note (it fills the note
// commitment, the VALID_INPUT proof, the anchor pool, and the signature).
placeOrder(order: object) {
return fetch(`${this.gateway}/orders`, {
method: "POST",
headers: this.auth(),
body: JSON.stringify(order),
}).then((r) => r.json());
}

cancelOrder(orderId: string, cancel: object) {
return fetch(`${this.gateway}/orders/${orderId}`, {
method: "DELETE",
headers: this.auth(),
body: JSON.stringify(cancel),
}).then((r) => r.json());
}

modifyOrder(orderId: string, modify: object) {
return fetch(`${this.gateway}/orders/${orderId}`, {
method: "PUT",
headers: this.auth(),
body: JSON.stringify(modify),
}).then((r) => r.json());
}

getOrder(orderId: string) {
return fetch(`${this.gateway}/orders/${orderId}`, { headers: this.auth() }).then((r) => r.json());
}

settlementStatus(batchId: string) {
return fetch(`${this.gateway}/settlement/status/${batchId}`, { headers: this.auth() }).then((r) => r.json());
}
}

Building an order with the SDK builders

The builders set the execution-policy fields (type, price limit, fill size, expiry) for a common intent. You merge a policy with the collateral the SDK derives from a deposited note.

// A resting bid, good for the next 10 minutes (GTT).
const { slot, unix_ms } = await client.serverTime();
const gttPolicy = gttLimitPolicy({
priceLimit: 150_000_000n,
serverSlot: slot,
serverUnixMs: unix_ms,
expiryUnixMs: Date.now() + 10 * 60 * 1000,
});

// A market bid: IOC capped at the worst price you'll pay.
const market = marketPolicy({ side: OrderSide.Bid, priceCap: 155_000_000n });

// An all-or-none resting bid.
const aon = aonPolicy({ amount: 10_000_000n, priceLimit: 150_000_000n });

// `proveAndBuildOrder` does the whole flow: fetch the note's inclusion witness
// from /tree/inclusion, generate the VALID_INPUT proof, then assemble + sign the
// wire body (note commitment, proof, anchor pool, trading-key signature). The
// prover is pluggable: `nodeValidInputProver` runs the compiled circuit via
// snarkjs in Node; a browser app supplies its own WASM prover.
const order = await proveAndBuildOrder({
baseUrl: GATEWAY,
token: client["token"]!,
prover: nodeValidInputProver({ wasmPath, zkeyPath }),
ownerCommitmentBlinding,
tokenMint,
masterSeed,
spendingKey,
ownerCommitment,
userCommitment,
tradingKey: trading.publicKey,
sign: (digest) => nacl.sign.detached(digest, trading.secretKey),
note, // { commitment, innerHash, amount }
symbol: "SOL-USDC",
side: OrderSide.Bid,
policy: gttPolicy,
amount: 10_000_000n,
orderId: deriveOrderId(masterSeed, 0),
});

// Submit over REST...
const res = await placeOrder({ baseUrl: GATEWAY, token: client["token"]! }, order);
console.log("placed", res.order_id, res.status);
Already hold a proof?

If you already have a VALID_INPUT proof (e.g. relayed from elsewhere), skip the prover and call buildOrder({ …, validInput: { proofBytes, merkleRoot } }) directly. proveAndBuildOrder is just the fetch-prove-build convenience on top.

Streaming order and fill events

// Per-account order lifecycle: partial / full fill, cancel, expiry.
const orders = subscribeOrderUpdates({
gatewayWsUrl: WSS,
token: client["token"]!,
onUpdate: (u) => {
if (u.kind === "partially_filled") console.log("partial", u.filled_quantity, "resting", u.new_amount);
if (u.kind === "fully_filled") console.log("filled", u.order_id);
},
onResync: () => console.warn("orders stream lagged, reconcile via GET /orders/:id"),
});

// Per-account fills: verified change-note memos (the SDK checks each memo's
// anchor + commitment binding before handing it to you).
const fills = subscribeFills({
gatewayWsUrl: WSS,
token: client["token"]!,
masterSeed,
ownerCommitment,
store: noteStore,
onFill: (rec) => console.log("change note stored", rec.commitment),
onResync: () => console.warn("fills stream lagged, backfill then reopen"),
});

Submitting over the trading socket

For a high-frequency client, submit orders over the WebSocket trading socket instead of REST, using one warm connection plus cancel-on-disconnect. The TradingClient correlates each reply to its request and resolves a promise per call:

const trader = new TradingClient({
gatewayWsUrl: WSS,
token: client["token"]!,
cancelOnDisconnect: true,
});
await trader.connect();

try {
const res = await trader.place(order); // resolves with the acceptance
console.log("accepted", res.order_id);
} catch (e) {
if (e instanceof DarknyxApiError) console.error(e.code, e.message); // numeric code
}

// Cancel + modify use the same socket; build the bodies with buildCancel / buildOrder.
const cancel = await buildCancel({ orderId, tradingKey: trading.publicKey, cancelNonce: 1n, sign });
await trader.cancel(hex(orderId), cancel);

Usage

const client = new DarknyxClient("https://<gateway-host>");
await client.login(API_KEY, API_SECRET, PASSPHRASE);

const status = await client.systemStatus();
if (status.degraded) throw new Error("venue degraded, back off");

const markets = await client.getInstruments();
console.log(markets.instruments.map((m) => m.symbol));

// build + place an order (see above), then watch its lifecycle on the streams.
Verify the engine first

For the full trust guarantee, verify the enclave's attestation against an expected measurement before sending order intent; the SDK ships a helper that runs the attestation chain for you. Skipping it gives you a private channel to a machine, not a verified one.