diff --git a/src/exchange/api.ts b/src/exchange/api.ts index b434bb9..d45ed28 100644 --- a/src/exchange/api.ts +++ b/src/exchange/api.ts @@ -1,32 +1,66 @@ import { Account, - AccountInfo, Blockhash, + AccountInfo, + Blockhash, Connection, Context, - PublicKey, Transaction, + LAMPORTS_PER_SOL, + PublicKey, + RpcResponseAndContext, + Transaction, } from "@solana/web3.js"; import { Coin, Dir, - Exchange, + Exchange, Fill, L2OrderBook, - MarketInfo, OrderType, + MarketInfo, + Order, + OrderInfo, + OrderType, + OwnOrders, Pair, RawTrade, - TimestampedL2Levels, TokenAccountInfo, + TimestampedL2Levels, + TokenAccountInfo, Trade, } from "./types"; import * as config from "../config"; -import {COIN_MINTS, EXCHANGE_ENABLED_MARKETS, MINT_COINS} from "./config"; -import {DirUtil, getKeys, getUnixTs, logger, sleep} from "../utils"; +import { COIN_MINTS, EXCHANGE_ENABLED_MARKETS, MINT_COINS } from "./config"; +import { + DirUtil, + divideBnToNumber, + getKeys, + getUnixTs, + logger, + sleep, +} from "../utils"; import assert from "assert"; -import {Market, OpenOrders, Orderbook, TokenInstructions} from "@project-serum/serum"; +import { + Market, + OpenOrders, + Orderbook, + TokenInstructions, +} from "@project-serum/serum"; +import { Order as SerumOrder } from "@project-serum/serum/lib/market"; import { Buffer } from "buffer"; import BN from "bn.js"; -import {makeClientOrderId, parseTokenAccountData} from "./utils"; -import {OrderParams} from "@project-serum/serum/lib/market"; -import {BLOCKHASH_CACHE_TIME, DEFAULT_TIMEOUT} from "../config"; -import {signAndSerializeTransaction} from "./solana"; +import { + getTokenMultiplierFromDecimals, + makeClientOrderId, + parseMintData, + parseTokenAccountData, +} from "./utils"; +import { OrderParams } from "@project-serum/serum/lib/market"; +import { BLOCKHASH_CACHE_TIME, DEFAULT_TIMEOUT } from "../config"; +import { + createRpcRequest, + GetMultipleAccountsAndContextRpcResult, + RpcRequest, + signAndSerializeTransaction, +} from "./solana"; +import { WRAPPED_SOL_MINT } from "@project-serum/serum/lib/token-instructions"; +import { parse as urlParse } from "url"; export class SerumApi { static readonly exchange: Exchange = "serum"; @@ -34,7 +68,7 @@ export class SerumApi { readonly exchange: Exchange; readonly marketInfo: { [market: string]: MarketInfo }; readonly markets: Pair[]; - readonly addressMarkets: { [address: string]: Market }; + readonly addressMarkets: { [address: string]: Pair }; readonly marketAddresses: { [market: string]: PublicKey }; readonly addressProgramIds: { [address: string]: PublicKey }; private _loadedMarkets: { [address: string]: Market }; @@ -47,6 +81,19 @@ export class SerumApi { [market: string]: { buy: TimestampedL2Levels; sell: TimestampedL2Levels }; }; private _wsOrderbooksConnected: string[]; + private _ownOrdersByMarket: { + [market: string]: { + orders: OwnOrders>; + fetchedAt: number; + }; + }; + private _openOrdersAccountCache: { + [market: string]: { + accounts: OpenOrders[]; + ts: number; + }; + }; + private _rpcRequest: RpcRequest; protected _tokenAccountsCache: { [coin: string]: { accounts: TokenAccountInfo[]; ts: number }; }; @@ -74,6 +121,9 @@ export class SerumApi { this._wsOrderbooksConnected = []; this._tokenAccountsCache = {}; this._blockhashCache = { blockhash: "", fetchedAt: 0 }; + this._ownOrdersByMarket = {}; + this._openOrdersAccountCache = {}; + this._rpcRequest = createRpcRequest(urlParse(url).href); this.marketInfo = marketInfo; this.markets = markets; this.marketAddresses = marketAddresses; @@ -279,7 +329,10 @@ export class SerumApi { return rawTrades.map((trade) => parseTrade(trade)); } - async getRestOrderBook(coin: Coin, priceCurrency: Coin): Promise { + async getRestOrderBook( + coin: Coin, + priceCurrency: Coin + ): Promise { const validAt = getUnixTs(); const marketAddress: PublicKey = this.getMarketAddress(coin, priceCurrency); const market = await this.getMarketFromAddress(marketAddress); @@ -465,7 +518,7 @@ export class SerumApi { transaction: Transaction, signers: Account[], transactionSignatureTimeout: number = DEFAULT_TIMEOUT, - onError?: (err) => void, + onError?: (err) => void ): Promise { const blockhash = await this.getCachedBlockhash(); const rawTransaction = await signAndSerializeTransaction( @@ -540,7 +593,7 @@ export class SerumApi { quantity: number, price: number, orderType: OrderType = OrderType.limit, - options: {[k: string]: unknown} = {} + options: { [k: string]: unknown } = {} ): Promise { const clientId = typeof options.clientId === "string" || @@ -582,7 +635,7 @@ export class SerumApi { quantity: number, price: number, orderType: OrderType = OrderType.limit, - options: {[k: string]: unknown} = {} + options: { [k: string]: unknown } = {} ): Promise<{ transaction: Transaction; signers: Account[] }> { logger.info( `Order parameters: ${side}, ${coin}, ${priceCurrency}, ${quantity}, ${price}, ${orderType}` @@ -598,9 +651,7 @@ export class SerumApi { } const [market, openOrdersAccount] = await Promise.all([ - this.getMarketFromAddress( - this.getMarketAddress(coin, priceCurrency) - ), + this.getMarketFromAddress(this.getMarketAddress(coin, priceCurrency)), this._getOpenOrdersAccountToUse(coin, priceCurrency), ]); @@ -692,6 +743,418 @@ export class SerumApi { ); } + async makeCancelByClientIdTransaction( + orderId: string, + coin: Coin, + priceCurrency: Coin + ): Promise<{ transaction: Transaction; signers: Account[] }> { + const accountsForMarket = await this.getOpenOrdersAccountsForMarket( + coin, + priceCurrency + ); + if (!accountsForMarket) { + throw Error( + `Could not find an open order accounts for market ${coin}/${priceCurrency}` + ); + } + + const m = new Pair(coin, priceCurrency); + const order = this.getOrderFromOwnOrdersCache(orderId, m); + let account = accountsForMarket.find( + (account) => account.address.toBase58() === order?.info.openOrdersAddress + ); + if (!order || !account) { + this.getOwnOrders(coin, priceCurrency); // update the cache in the background + // 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.` + ); + } + logger.info( + `Cancelling ${orderId} using account ${account.publicKey.toBase58()}` + ); + + const market = await this.getMarketFromAddress( + this.getMarketAddress(coin, priceCurrency) + ); + const txn = await market.makeCancelOrderByClientIdTransaction( + this._connection, + this._publicKey, + account.address, + new BN(orderId) + ); + txn.add(market.makeMatchOrdersTransaction(5)); + const signers = [new Account(this._privateKey)]; + return { + transaction: txn, + signers, + }; + } + + getOrderFromOwnOrdersCache( + orderId: string, + market: Pair + ): Order | null { + const cachedOwnOrders = this._ownOrdersByMarket[market.key()]?.orders || {}; + let usableOrderId; + if (orderId in cachedOwnOrders) { + // use orderId to cancel + usableOrderId = orderId; + } else if ( + Object.values(cachedOwnOrders) + .map((order) => order.info.clientId) + .includes(orderId) + ) { + // orderid is client id, + usableOrderId = Object.values(cachedOwnOrders).filter( + (order) => order.info.clientId === orderId + )[0].info.orderId; + } else { + return null; + } + return cachedOwnOrders[usableOrderId]; + } + + async getOwnOrders( + coin?: Coin, + priceCurrency?: Coin + ): Promise>> { + if (coin && priceCurrency) { + const market = await this.getMarketFromAddress( + this.getMarketAddress(coin, priceCurrency) + ); + const fetchedAt = getUnixTs(); + const [bids, asks] = await Promise.all([ + market.loadBids(this._connection), + market.loadAsks(this._connection), + ]); + const openOrdersAccounts = await this.getOpenOrdersAccountsForMarket( + coin, + priceCurrency + ); + const rawOrders = await market.filterForOpenOrders( + bids, + asks, + openOrdersAccounts + ); + const orders = this.parseRawOrders(rawOrders, coin, priceCurrency); + + this._ownOrdersByMarket[new Pair(coin, priceCurrency).key()] = { + orders, + fetchedAt, + }; + return orders; + } + return Promise.all( + this.markets.map((market) => + this.getOwnOrders(market.coin, market.priceCurrency) + ) + ).then((orders) => + orders.reduce((acc, curr) => { + return { ...acc, ...curr }; + }) + ); + } + + parseRawOrders( + rawOrders: SerumOrder[], + coin: Coin, + priceCurrency: Coin + ): OwnOrders> { + return Object.fromEntries( + rawOrders.map((order) => [ + order.orderId, + { + exchange: this.exchange, + coin: coin, + priceCurrency: priceCurrency, + side: DirUtil.parse(order.side), + price: order.price, + quantity: order.size, + info: new OrderInfo( + order.orderId.toString(), + order.openOrdersAddress.toBase58(), + order.openOrdersSlot, + order.price, + order.priceLots.toString(), + order.size, + order.sizeLots.toString(), + order.side, + order.clientId ? order.clientId.toString() : "", + order.feeTier + ), + }, + ]) + ); + } + + async getFills(coin?: Coin, priceCurrency?: Coin): Promise { + if (coin && priceCurrency) { + const market = await this.getMarketFromAddress( + this.getMarketAddress(coin, priceCurrency) + ); + const rawFills = await market.loadFills(this._connection); + const openOrdersAccount = ( + await this.getOpenOrdersAccountsForMarket(coin, priceCurrency) + ).map((account) => account.address.toBase58()); + const ourFills = rawFills.filter((rawFill) => + openOrdersAccount.includes(rawFill.openOrders.toBase58()) + ); + return this.parseRawFills(ourFills, coin, priceCurrency); + } + return Promise.all( + this.markets.map((market) => + this.getFills(market.coin, market.priceCurrency) + ) + ).then((fills) => fills.reduce((acc, curr) => [...acc, ...curr])); + } + + parseRawFills( + rawFills: RawTrade[], + coin: Coin, + priceCurrency: Coin + ): Fill[] { + const time = getUnixTs(); + const parseFill = (rawFill): Fill => { + return { + exchange: this.exchange, + coin: coin, + priceCurrency: priceCurrency, + side: DirUtil.parse(rawFill.side), + price: parseFloat(rawFill.price), + quantity: parseFloat(rawFill.size), + orderId: rawFill.orderId.toString(), + fee: rawFill.feeCost, + time: time, + info: { + ...rawFill.eventFlags, + openOrdersSlot: rawFill.openOrdersSlot, + quantityReleased: rawFill.nativeQuantityReleased.toString(), + quantityPaid: rawFill.nativeQuantityPaid.toString(), + openOrders: rawFill.openOrders.toBase58(), + clientId: rawFill.clientOrderId.toString(), + feeOrRebate: rawFill.nativeFeeOrRebate.toString(), + }, + }; + }; + return rawFills.map((rawFill) => parseFill(rawFill)); + } + + async getBalances(): Promise<{ + [key: string]: { + mintKey: string; + coin: string; + total: number; + free: number; + }; + }> { + const [tokenAccounts, openOrdersAccounts] = await Promise.all([ + this.getTokenAccounts(undefined, 60), + this.getOpenOrdersAccounts(undefined, 60), + ]); + + const accountsByCoin: { + [coin: string]: { + mint: PublicKey; + tokenAccounts: PublicKey[]; + openOrdersAccounts: { [key: string]: Pair }; + }; + } = {}; + for (const [marketKey, marketOpenOrdersAccounts] of Object.entries( + openOrdersAccounts + )) { + for (const openOrdersAccount of marketOpenOrdersAccounts) { + const market = Pair.fromKey(marketKey); + const key = openOrdersAccount.publicKey.toBase58(); + if (!(market.coin in accountsByCoin)) { + accountsByCoin[market.coin] = { + mint: new PublicKey(COIN_MINTS[market.coin]), + tokenAccounts: [], + openOrdersAccounts: {}, + }; + } + if (!(market.priceCurrency in accountsByCoin)) { + accountsByCoin[market.priceCurrency] = { + mint: new PublicKey(COIN_MINTS[market.priceCurrency]), + tokenAccounts: [], + openOrdersAccounts: {}, + }; + } + accountsByCoin[market.coin].openOrdersAccounts[key] = market; + accountsByCoin[market.priceCurrency].openOrdersAccounts[key] = market; + } + } + for (const tokenAccount of tokenAccounts) { + const coin = MINT_COINS[tokenAccount.mint.toBase58()]; + if (!(coin in accountsByCoin)) { + accountsByCoin[coin] = { + mint: tokenAccount.mint, + tokenAccounts: [], + openOrdersAccounts: {}, + }; + } + accountsByCoin[coin].tokenAccounts.push(tokenAccount.pubkey); + } + if (!("SOL" in accountsByCoin)) { + accountsByCoin["SOL"] = { + mint: WRAPPED_SOL_MINT, + tokenAccounts: [], + openOrdersAccounts: {}, + }; + } + accountsByCoin["SOL"].tokenAccounts.push(this._publicKey); + accountsByCoin["SOL"].mint = TokenInstructions.WRAPPED_SOL_MINT; + + const coins = Object.keys(accountsByCoin); + const accountContents = await Promise.all( + coins.map((coin) => + this.getMultipleSolanaAccounts([ + accountsByCoin[coin].mint, + ...accountsByCoin[coin].tokenAccounts, + ...Object.keys(accountsByCoin[coin].openOrdersAccounts).map( + (stringKey) => new PublicKey(stringKey) + ), + ]) + ) + ); + const accountContentsByCoin = Object.fromEntries( + coins.map((coin, i) => [coin, accountContents[i]]) + ); + const balances = {}; + Object.entries(accountContentsByCoin).forEach(([coin, accountsInfo]) => { + const mintValue = + accountsInfo.value[accountsByCoin[coin].mint.toBase58()]; + if (mintValue === null) { + return; + } + const mint = parseMintData(mintValue.data); + const ooFree = {}; + const ooTotal = {}; + for (const openOrdersAccountKey of Object.keys( + accountsByCoin[coin].openOrdersAccounts + )) { + const accountValue = accountsInfo.value[openOrdersAccountKey]; + if (accountValue === null) { + continue; + } + const market = + accountsByCoin[coin].openOrdersAccounts[openOrdersAccountKey]; + const parsedAccount = OpenOrders.fromAccountInfo( + new PublicKey(openOrdersAccountKey), + accountValue, + this.marketInfo[market.key()].programId + ); + if (coin == market.coin) { + ooFree[coin] = ooFree[coin] + ? parsedAccount.baseTokenFree.add(ooFree[coin]) + : parsedAccount.baseTokenFree; + ooTotal[coin] = ooTotal[coin] + ? parsedAccount.baseTokenTotal.add(ooTotal[coin]) + : parsedAccount.baseTokenTotal; + } else { + ooFree[coin] = ooFree[coin] + ? parsedAccount.quoteTokenFree.add(ooFree[coin]) + : parsedAccount.quoteTokenFree; + ooTotal[coin] = ooTotal[coin] + ? parsedAccount.quoteTokenTotal.add(ooTotal[coin]) + : parsedAccount.quoteTokenTotal; + } + } + let total = 0; + let free = 0; + for (const tokenAccountKey of accountsByCoin[coin].tokenAccounts) { + const accountValue = accountsInfo.value[tokenAccountKey.toBase58()]; + if (accountValue === null) { + continue; + } + if (coin === "SOL") { + total += (accountValue.lamports ?? 0) / LAMPORTS_PER_SOL; + free += total; + } else { + const parsedAccount = parseTokenAccountData(accountValue.data); + const additionalAmount = divideBnToNumber( + new BN(parsedAccount.amount), + getTokenMultiplierFromDecimals(mint.decimals) + ); + total += additionalAmount; + free += additionalAmount; + } + } + if (ooFree[coin]) { + free += divideBnToNumber( + ooFree[coin], + getTokenMultiplierFromDecimals(mint.decimals) + ); + } + if (ooTotal[coin]) { + total += divideBnToNumber( + ooTotal[coin], + getTokenMultiplierFromDecimals(mint.decimals) + ); + } + balances[coin] = { + mintKey: accountsByCoin[coin].mint.toBase58(), + coin, + total, + free, + }; + }); + return balances; + } + + async getOpenOrdersAccounts( + market?: Pair, + cacheDurationSec = 2 + ): Promise<{ [market: string]: OpenOrders[] }> { + let serumMarkets: Market[]; + if (market) { + serumMarkets = [ + await this.getMarketFromAddress( + this.getMarketAddress(market.coin, market.priceCurrency) + ), + ]; + } else { + serumMarkets = await Promise.all( + this.markets.map((market) => + this.getMarketFromAddress( + this.getMarketAddress(market.coin, market.priceCurrency) + ) + ) + ); + } + + const now = getUnixTs(); + const openOrdersAccounts: { + [market: string]: OpenOrders[]; + } = await Promise.all( + serumMarkets.map((serumMarket) => + serumMarket.findOpenOrdersAccountsForOwner( + this._connection, + this._publicKey, + cacheDurationSec * 1000 + ) + ) + ) + .then((openOrdersAccounts) => + openOrdersAccounts.reduce((r, a) => r.concat(a), []) + ) + .then((openOrdersAccounts) => { + return openOrdersAccounts.reduce((rv, account) => { + const market = this.addressMarkets[account.market.toBase58()].key(); + (rv[market] = rv[market] || []).push(account); + return rv; + }, {}); + }); + for (const [marketKey, openOrders] of Object.entries(openOrdersAccounts)) { + this._openOrdersAccountCache[marketKey] = { + accounts: openOrders, + ts: now, + }; + } + return openOrdersAccounts; + } + async getTokenAccounts( coin?: Coin, cacheDurationSecs = 0 @@ -736,4 +1199,110 @@ export class SerumApi { } return cache[coin].accounts; } + + async getMultipleSolanaAccounts( + publicKeys: PublicKey[] + ): Promise< + RpcResponseAndContext<{ [key: string]: AccountInfo | null }> + > { + const args = [ + publicKeys.map((k) => k.toBase58()), + { commitment: "recent" }, + ]; + const unsafeRes = await this._rpcRequest("getMultipleAccounts", args); + const res = GetMultipleAccountsAndContextRpcResult(unsafeRes); + if (res.error) { + throw new Error( + "failed to get info about accounts " + + publicKeys.map((k) => k.toBase58()).join(", ") + + ": " + + res.error.message + ); + } + assert(typeof res.result !== "undefined"); + const accounts: Array<{ + executable: any; + owner: PublicKey; + lamports: any; + data: Buffer; + } | null> = []; + for (const account of res.result.value) { + let value: { + executable: any; + owner: PublicKey; + lamports: any; + data: Buffer; + } | null = null; + if (res.result.value) { + const { executable, owner, lamports, data } = account; + assert(data[1] === "base64"); + value = { + executable, + owner: new PublicKey(owner), + lamports, + data: Buffer.from(data[0], "base64"), + }; + } + accounts.push(value); + } + return { + context: { + slot: res.result.context.slot, + }, + value: Object.fromEntries( + accounts.map((account, i) => [publicKeys[i].toBase58(), account]) + ), + }; + } + + async settleFunds(coin: Coin, priceCurrency: Coin): Promise { + const market = await this.getMarketFromAddress( + this.getMarketAddress(coin, priceCurrency) + ); + const promises: Promise[] = []; + for (const openOrders of await this.getOpenOrdersAccountsForMarket( + coin, + priceCurrency + )) { + if ( + openOrders.baseTokenFree.gt(new BN("0")) || + openOrders.quoteTokenFree.gt(new BN("0")) + ) { + // spl-token accounts to which to send the proceeds from trades + let baseTokenAccount; + let quoteTokenAccount; + if (coin == "SOL") { + const priceCurrencyTokenAccount = await this.getTokenAccounts( + priceCurrency, + 60 + ); + baseTokenAccount = this._publicKey; + quoteTokenAccount = priceCurrencyTokenAccount[0].pubkey; + } else { + const [ + coinTokenAccount, + priceCurrencyTokenAccount, + ] = await Promise.all([ + this.getTokenAccounts(coin, 60), + this.getTokenAccounts(priceCurrency, 60), + ]); + baseTokenAccount = coinTokenAccount[0].pubkey; + quoteTokenAccount = priceCurrencyTokenAccount[0].pubkey; + } + logger.debug(`Settling funds on ${coin}/${priceCurrency}`); + promises.push( + market + .settleFunds( + this._connection, + new Account(this._privateKey), + openOrders, + baseTokenAccount, + quoteTokenAccount + ) + .then((txid) => this.awaitTransactionSignatureConfirmation(txid)) + ); + } + } + await Promise.all(promises); + } } diff --git a/src/exchange/types.ts b/src/exchange/types.ts index 7d6aa93..f091edd 100644 --- a/src/exchange/types.ts +++ b/src/exchange/types.ts @@ -1,6 +1,6 @@ import { PublicKey } from "@solana/web3.js"; -import { Order as SerumOwnOrder } from "@project-serum/serum/lib/market"; import BN from "bn.js"; +import { Order as SerumOrder } from "@project-serum/serum/lib/market"; export type Coin = string; export type Exchange = string; @@ -78,6 +78,73 @@ export class Order { } } +export class OrderInfo { + orderId: string; + openOrdersAddress: string; + openOrdersSlot: number; + price: number; + priceLots: string; + size: number; + sizeLots: string; + side: "buy" | "sell"; + clientId: string; + feeTier: number; + + constructor( + orderId: string, + openOrdersAddress: string, + openOrdersSlot: number, + price: number, + priceLots: string, + size: number, + sizeLots: string, + side: "buy" | "sell", + clientId: string, + feeTier: number + ) { + this.orderId = orderId; + this.openOrdersAddress = openOrdersAddress; + this.openOrdersSlot = openOrdersSlot; + this.price = price; + this.priceLots = priceLots; + this.size = size; + this.sizeLots = sizeLots; + this.side = side; + this.clientId = clientId; + this.feeTier = feeTier; + } + + static fromSerumOrder(order: SerumOrder): OrderInfo { + return new OrderInfo( + order.orderId.toString(), + order.openOrdersAddress.toBase58(), + order.openOrdersSlot, + order.price, + order.priceLots.toString(), + order.size, + order.sizeLots.toString(), + order.side, + order.clientId ? order.clientId.toString() : "", + order.feeTier + ); + } + + toSerumOrder(): SerumOrder { + return { + orderId: new BN(this.orderId), + openOrdersAddress: new PublicKey(this.openOrdersAddress), + openOrdersSlot: this.openOrdersSlot, + price: this.price, + priceLots: new BN(this.priceLots), + size: this.size, + sizeLots: new BN(this.sizeLots), + side: this.side, + clientId: new BN(this.clientId), + feeTier: this.feeTier, + }; + } +} + export interface Trade { exchange: Exchange; coin: Coin; @@ -101,8 +168,6 @@ export interface Fill { time: number; orderId: string; fee: number; - feeCurrency: Coin; - liquidity: Liquidity; info?: T; } @@ -147,73 +212,6 @@ export interface RawTrade { nativeFeeOrRebate: BN; } -export class SerumOrder { - orderId: string; - openOrdersAddress: string; - openOrdersSlot: number; - price: number; - priceLots: string; - size: number; - sizeLots: string; - side: "buy" | "sell"; - clientId: string; - feeTier: number; - - constructor( - orderId: string, - openOrdersAddress: string, - openOrdersSlot: number, - price: number, - priceLots: string, - size: number, - sizeLots: string, - side: "buy" | "sell", - clientId: string, - feeTier: number - ) { - this.orderId = orderId; - this.openOrdersAddress = openOrdersAddress; - this.openOrdersSlot = openOrdersSlot; - this.price = price; - this.priceLots = priceLots; - this.size = size; - this.sizeLots = sizeLots; - this.side = side; - this.clientId = clientId; - this.feeTier = feeTier; - } - - static fromSerumOrder(order: SerumOwnOrder): SerumOrder { - return new SerumOrder( - order.orderId.toString(), - order.openOrdersAddress.toBase58(), - order.openOrdersSlot, - order.price, - order.priceLots.toString(), - order.size, - order.sizeLots.toString(), - order.side, - order.clientId ? order.clientId.toString() : "", - order.feeTier - ); - } - - toSerumOrder(): SerumOwnOrder { - return { - orderId: new BN(this.orderId), - openOrdersAddress: new PublicKey(this.openOrdersAddress), - openOrdersSlot: this.openOrdersSlot, - price: this.price, - priceLots: new BN(this.priceLots), - size: this.size, - sizeLots: new BN(this.sizeLots), - side: this.side, - clientId: new BN(this.clientId), - feeTier: this.feeTier, - }; - } -} - export interface TimestampedL2Levels { orderbook: [number, number][]; receivedAt: number; diff --git a/src/exchange/utils.ts b/src/exchange/utils.ts index 984b7a2..68e6baf 100644 --- a/src/exchange/utils.ts +++ b/src/exchange/utils.ts @@ -91,3 +91,7 @@ export function makeClientOrderId(bits = 64): BN { } return new BN(binaryString, 2); } + +export function getTokenMultiplierFromDecimals(decimals: number): BN { + return new BN(10).pow(new BN(decimals)); +}