Everything implemented
This commit is contained in:
parent
ba165df268
commit
e219932ad2
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue