Everything implemented

This commit is contained in:
Nathaniel Parke 2020-11-11 17:43:04 +08:00
parent ba165df268
commit e219932ad2
3 changed files with 661 additions and 90 deletions

View File

@ -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<Order<OrderInfo>>;
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<L2OrderBook> {
async getRestOrderBook(
coin: Coin,
priceCurrency: Coin
): Promise<L2OrderBook> {
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<string> {
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<string> {
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<OrderInfo> | 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<OwnOrders<Order<OrderInfo>>> {
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<Order<OrderInfo>> {
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<Fill[]> {
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<Buffer> | 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<void> {
const market = await this.getMarketFromAddress(
this.getMarketAddress(coin, priceCurrency)
);
const promises: Promise<string>[] = [];
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);
}
}

View File

@ -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<T = any> {
}
}
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<T = any> {
exchange: Exchange;
coin: Coin;
@ -101,8 +168,6 @@ export interface Fill<T = any> {
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;

View File

@ -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));
}