@alcorexchange/orderbook-sdk
v0.1.1
Published
Client SDK for the Alcor orderbook contract: units, actions, reads, events, signing.
Maintainers
Readme
@alcorexchange/orderbook-sdk
Client SDK for the Alcor orderbook contract on Antelope chains (Telos). Exact unit conversions, action builders for any wallet, table and read-only reads, transaction signing for bots, event decoding and a local order book kept current by events.
npm install @alcorexchange/orderbook-sdk @wharfkit/antelopeESM only: Node 20.10+ or any modern bundler (it imports JSON with an import attribute). Every 64-bit value is a bigint.
Quick start
import { OrderbookClient, OrderbookActions, KeySigner, Rpc, Side, OrderFlag, priceToUnits, baseToLots } from '@alcorexchange/orderbook-sdk';
const rpc = new Rpc('https://telos.example');
const client = new OrderbookClient({ rpc, contract: 'orderbook' });
const actions = new OrderbookActions('orderbook');
const m = await client.market(1n); // lot size, tick, tokens, fees
const order = actions.placeOrder({
owner: 'alice', marketId: m.id, side: Side.BUY,
price: priceToUnits(m, '0.0500'), qty: baseToLots(m, '100'), flags: OrderFlag.POST_ONLY,
});
// Wallet users: session.transact({ actions: [order] }). Bots sign with a key:
const signer = new KeySigner({ rpc, privateKey: process.env.KEY! });
const { results, events } = await signer.transact([order]);Units
A market trades qty lots of lot_size minimal base units. price is minimal quote units
per lot, on a grid of tick_size. An order of qty lots at price moves qty * price quote
units. Parsing is exact: a value off the token precision, the lot size or the tick grid
throws instead of rounding. unitsToPrice floors only when asked for fewer decimals.
| helper | from → to |
|---|---|
| priceToUnits(m, '0.05'), unitsToPrice(m, units) | human price ↔ contract price |
| baseToLots(m, '100'), lotsToBase(m, lots) | base amount ↔ lots |
| quoteToUnits(m, '12.5'), unitsToQuote(m, units) | quote amount ↔ minimal units |
| parseUnits, formatUnits, assetString | any token amount |
Funds
Funds live in an internal ledger. Deposit with a plain token transfer to the contract
(actions.deposit). Orders lock what they need: a sell locks its base, a buy locks
qty * price plus the taker fee (buyReserve). actions.withdraw returns two actions: the
token's open for the recipient, paid by the withdrawer, and the withdraw itself, because
the contract only pays out to an existing token row.
Actions
OrderbookActions builds action JSON for any wallet (wharfkit Session, Anchor, a key):
deposit,withdrawplaceOrderwithOrderFlag.POST_ONLY,IOC,FOKorPOST_ONLY_SKIPand an optionalexpiresAt(seconds).POST_ONLY_SKIPis a post-only that, where it would trade, places nothing and does not fail (order_id = 0,rested = 0, no event), so a batch of cancels and new quotes survives a quote the price ran over. Other errors (funds, tick,min_notional) still fail the transaction.buyQuotespends at most a quote amount, fees included; nothing restscancel(anyone may cancel an expired order; its funds go to its owner),reduceOrder,cancelAllplaceTrigger,placeOco,cancelTrigger,crankswaptrades straight from the wallet, see below
A remainder below the market's min_notional never rests in the book (isDust).
Swap
A transfer to the contract with memo swap#<market_id>#<min_out>[#<recipient>] trades at once
and leaves nothing on the exchange: base in sells whole lots, quote in buys for the whole
amount, fees included, both without a price limit. The output goes to the recipient (the
sender by default) and whatever did not trade, including a part below one lot, comes back to
the sender in the same transaction. If the output is below min_out (minimal units of the
output token) the whole transaction fails.
const book = new BookState(m.id, [
...(await client.book(m.id, Side.BUY, { includeExpired: true })),
...(await client.book(m.id, Side.SELL, { includeExpired: true })),
]);
const q = quoteSwap(book, m, { owner: 'alice', side: Side.SELL, units: 5_5000n, now: Math.floor(Date.now() / 1000) });
const minOut = (q.received * 997n) / 1000n; // 0.3% slippage
await session.transact({ actions: actions.swap({ from: 'alice', marketId: m.id, token: base, out: quote, units: 5_5000n, minOut }) });actions.swap returns the token's open for the recipient's output row (paid by the
sender, a no-op when it exists) and the transfer. quoteSwap runs the contract's arithmetic
on a book copy; give it expired orders too, they cost work. toSwapEvent reads the swap
event: who, to whom, side, input spent, input returned, output sent.
Reads
OrderbookClient reads any API node:
markets(),market(id),tokens(),config()book(marketId, side)(expired orders hidden),depth(marketId, side, levels)balances(owner),ordersOf(owner),triggersOf(owner)triggerQueue(marketId, op),referencePrice(marketId)readOnly(name, data)for the contract's read-only actions
Triggers
Stop and take-profit orders lock their funds when placed. They fire when the market's
reference price reaches the trigger price. The reference is the median of the last trade
prices of the last three blocks that traded. A trade fires due triggers on its own; crank
fires the rest (triggerDue, referencePrice mirror the contract).
Events
The contract logs every change as an events action. KeySigner.transact returns the events
of its own transaction (eventsFromTraces); decodeEvents decodes them from state history.
BookState keeps a local order book from a snapshot and events. It throws BookGapError
when an event seq is missing; take a new snapshot then. With the Alcor orderbook indexer,
the snapshot is GET <api>/markets/:id/book and the events come over <api>/ws, where <api> is
https://telos.alcor.exchange/api/v2/orderbook on Telos; toOrder and
toEvent turn their JSON (64-bit values as strings) into SDK types:
import { BookState, Side, toEvent, toOrder } from '@alcorexchange/orderbook-sdk';
const api = 'https://telos.alcor.exchange/api/v2/orderbook';
const snap = await fetch(`${api}/markets/1/book`).then((r) => r.json());
const book = new BookState(1n, snap.orders.map(toOrder), BigInt(snap.seq));
const ws = new WebSocket(`${api.replace('https', 'wss')}/ws`);
ws.onopen = () => ws.send(JSON.stringify({ op: 'subscribe', channel: 'market:1' }));
ws.onmessage = (msg) => {
const { events } = JSON.parse(msg.data);
for (const e of events ?? []) book.apply(toEvent(e)); // BookGapError: reload the snapshot
render(book.levels(Side.BUY, 20), book.levels(Side.SELL, 20));
};Events at or below the snapshot's seq are skipped. A block that lands between the snapshot
and the subscription shows up as a BookGapError on the next event, never as a wrong book.
Contract math
buyReserve, feeOf, affordableLots, orderLock, isDust, referencePrice, triggerDue,
quoteSwap compute exactly what the contract does, for previews before signing.
