diff --git a/.prettierignore b/.prettierignore index 3b4e8dc..c3b7eb3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ lib/ node_modules/ +secrets.json diff --git a/src/exchange/api.ts b/src/exchange/api.ts index d45ed28..3908069 100644 --- a/src/exchange/api.ts +++ b/src/exchange/api.ts @@ -12,7 +12,8 @@ import { import { Coin, Dir, - Exchange, Fill, + Exchange, + Fill, L2OrderBook, MarketInfo, Order, @@ -743,6 +744,27 @@ export class SerumApi { ); } + async cancelByClientId( + orderId: string, + coin: Coin, + priceCurrency: Coin + ): Promise { + const { transaction, signers } = await this.makeCancelByClientIdTransaction( + orderId, + coin, + priceCurrency + ); + const txid = await this.sendTransaction( + transaction, + signers, + DEFAULT_TIMEOUT, + () => {} + ); + logger.debug( + `finished sending cancel transaction:\n${orderId}\ntxid ${txid}` + ); + } + async makeCancelByClientIdTransaction( orderId: string, coin: Coin, @@ -768,7 +790,8 @@ export class SerumApi { // Assume we sent with lowest sort open orders account account = accountsForMarket.sort(this.compareOpenOrdersAccounts)[0]; logger.debug( - `Did not find order (${orderId}) in open order accounts. Using ${account.publicKey.toBase58()} as account.` + `Did not find order (${orderId}) in open order accounts. + Using ${account.publicKey.toBase58()} as account.` ); } logger.info( @@ -792,6 +815,59 @@ export class SerumApi { }; } + async cancelByStandardOrderId( + orderId: string, + coin: Coin, + priceCurrency: Coin + ): Promise { + const { + transaction, + signers, + } = await this.makeCancelByStandardIdTransaction( + orderId, + coin, + priceCurrency + ); + const txid = await this._connection.sendTransaction(transaction, signers, { + skipPreflight: true, + }); + await this.awaitTransactionSignatureConfirmation(txid); + } + + async makeCancelByStandardIdTransaction( + orderId: string, + coin: Coin, + priceCurrency: Coin + ): Promise<{ transaction: Transaction; signers: Account[] }> { + const market = new Pair(coin, priceCurrency); + let order = this.getOrderFromOwnOrdersCache(orderId, market); + if (!order) { + this.getOwnOrders(coin, priceCurrency); + order = this.getOrderFromOwnOrdersCache(orderId, market); + if (!order) { + throw Error("Could not find order for cancellation."); + } + } + logger.info( + `Cancelling ${orderId} ${coin} ${priceCurrency} using orderId ${order.info.orderId}` + ); + + const serumMarket = await this.getMarketFromAddress( + this.getMarketAddress(coin, priceCurrency) + ); + const transaction = await serumMarket.makeCancelOrderTransaction( + this._connection, + this._publicKey, + order.info.toSerumOrder() + ); + transaction.add(serumMarket.makeMatchOrdersTransaction(5)); + const signers = [new Account(this._privateKey)]; + return { + transaction, + signers, + }; + } + getOrderFromOwnOrdersCache( orderId: string, market: Pair @@ -910,11 +986,7 @@ export class SerumApi { ).then((fills) => fills.reduce((acc, curr) => [...acc, ...curr])); } - parseRawFills( - rawFills: RawTrade[], - coin: Coin, - priceCurrency: Coin - ): Fill[] { + parseRawFills(rawFills: RawTrade[], coin: Coin, priceCurrency: Coin): Fill[] { const time = getUnixTs(); const parseFill = (rawFill): Fill => { return { diff --git a/src/routes.ts b/src/routes.ts index 3d16147..d37471c 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,7 +1,8 @@ import express from "express"; -import { SerumApi } from "./exchange/api"; import expressAsyncHandler from "express-async-handler"; -import { logger } from "./utils"; +import { DirUtil, logger } from "./utils"; +import { SerumApi } from "./exchange"; +import { Coin, OrderType } from "./exchange/types"; const router = express.Router(); let api: SerumApi; @@ -32,4 +33,211 @@ router.get( }) ); +router.get( + "/orderbook/:coin-:quote", + expressAsyncHandler(async (req, res, next) => { + logger.info("Received request to api getOrderbook"); + api + .getWsOrderBook(req.params.coin, req.params.quote) + .then((orderBook) => res.send({ status: "ok", data: orderBook })) + .catch((err) => next(err)); + }) +); + +router.get( + "/trades/:coin-:quote", + expressAsyncHandler(async (req, res, next) => { + logger.info("Received request to api getTrades"); + api + .getTrades(req.params.coin, req.params.quote) + .then((trades) => res.send({ status: "ok", data: trades })) + .catch((err) => { + logger.info(err); + next(err); + }); + }) +); + +router.post( + "/place_order", + expressAsyncHandler(async (req, res, next) => { + logger.info(`Order parameters ${JSON.stringify(req.body)}`); + api + .placeOrder( + DirUtil.parse(req.body.side), + req.body.coin, + req.body.priceCurrency, + req.body.quantity, + req.body.price, + OrderType[req.body.orderType], + { + clientId: req.body.clientId, + orderEdge: req.body.orderEdge, + } + ) + .then((id) => res.send({ status: "ok", data: { id: id } })) + .catch((err) => { + logger.log("error", `${req.params.exchange} make_order error: ${err}`); + try { + const body = { + status: "error", + data: { + errorType: err.name, + errorMessage: err.message || JSON.stringify(err), + stack: (err.stack && err.stack.toString()) || JSON.stringify(err), + }, + }; + res.send(body); + } catch (e) { + try { + const body = { + status: "error", + data: { errorMessage: err, name: undefined, stack: undefined }, + }; + res.send(body); + } catch (f) { + next(err); + } + } + }); + }) +); + +router.post( + "/cancel", + expressAsyncHandler(async (req, res, next) => { + const coin = req.body.coin; + const priceCurrency = req.body.priceCurrency; + const orderId = req.body.orderId; + const clientOrderId = req.body.clientOrderId; + + if (!coin) { + const body = { + status: "error", + data: { + errorMessage: "Coin parameter missing from cancel request", + }, + }; + res.send(body); + } else if (!priceCurrency) { + const body = { + status: "error", + data: { + errorMessage: "Price currency parameter missing from cancel request", + }, + }; + res.send(body); + } else if (!orderId && !clientOrderId) { + const body = { + status: "error", + data: { + errorMessage: + "Order id and client order id missing from cancel request", + }, + }; + res.send(body); + } + + let cancelFn: ( + orderId: string, + coin: Coin, + priceCurrency: Coin + ) => Promise; + if (clientOrderId) { + cancelFn = api.cancelByClientId; + } else { + cancelFn = api.cancelByStandardOrderId; + } + cancelFn(req.params.orderId, req.params.coin, req.params.priceCurrency) + .then((result) => res.send({ status: "ok", data: {} })) + .catch((err) => { + logger.error( + `${req.params.coin}/${req.params.priceCurrency} cancel + ${req.params.orderId} received error ${JSON.stringify(err)}` + ); + try { + const body = { + status: "error", + data: { + errorType: err.name, + errorMessage: err.message || JSON.stringify(err), + stack: (err.stack && err.stack.toString()) || JSON.stringify(err), + }, + }; + res.send(body); + } catch (e) { + try { + const body = { + status: "error", + data: { errorMessage: err, name: undefined, stack: undefined }, + }; + res.send(body); + } catch (f) { + next(err); + } + } + }); + }) +); + +router.get( + "/own_orders/:coin-:quote", + expressAsyncHandler(async (req, res, next) => { + api + .getOwnOrders(req.params.coin, req.params.priceCurrency) + .then((orders) => res.send({ status: "ok", data: { orders } })) + .catch((err) => { + logger.log( + "error", + `Call to own_orders encountered error ${err.name}: \n ${err.stack}` + ); + res.send({ + status: "error", + data: { + errorType: err.name, + errorMessage: err.message, + stack: err.stack.toString(), + }, + }); + }); + }) +); + +router.get( + "/fills/:coin-:quote", + expressAsyncHandler(async (req, res, next) => { + if ("coin" in req.query && "priceCurrency" in req.query) { + api + .getFills(req.params.coin, req.params.priceCurrency) + .then((fills) => res.send({ status: "ok", data: fills })) + .catch((err) => next(err)); + } else { + api + .getFills() + .then((fills) => res.send({ status: "ok", data: fills })) + .catch((err) => next(err)); + } + }) +); + +router.get( + "/balances", + expressAsyncHandler(async (req, res, next) => { + api + .getBalances() + .then((balances) => res.send({ status: "ok", data: balances })) + .catch((err) => next(err)); + }) +); + +router.post( + "/settle", + expressAsyncHandler(async (req, res, next) => { + api + .settleFunds(req.body.coin, req.body.priceCurrency) + .then((_) => res.send({ status: "ok", data: {} })) + .catch((err) => next(err)); + }) +); + export { router as default };