2020-08-10 09:26:22 -07:00
|
|
|
import { blob, seq, struct, u8 } from 'buffer-layout';
|
2020-08-13 07:24:51 -07:00
|
|
|
import { accountFlagsLayout, publicKeyLayout, u128, u64 } from './layout';
|
2020-08-13 10:11:40 -07:00
|
|
|
import { Slab, SLAB_LAYOUT } from './slab';
|
2020-08-12 11:45:42 -07:00
|
|
|
import { DEX_PROGRAM_ID, DexInstructions } from './instructions';
|
2020-08-10 06:06:31 -07:00
|
|
|
import BN from 'bn.js';
|
2020-08-12 11:45:42 -07:00
|
|
|
import {
|
|
|
|
Account,
|
2020-08-13 10:11:40 -07:00
|
|
|
AccountInfo,
|
|
|
|
Connection,
|
2020-08-12 11:45:42 -07:00
|
|
|
PublicKey,
|
|
|
|
SystemProgram,
|
|
|
|
Transaction,
|
2020-08-22 23:13:20 -07:00
|
|
|
TransactionInstruction,
|
2020-08-18 03:16:08 -07:00
|
|
|
TransactionSignature,
|
2020-08-12 11:45:42 -07:00
|
|
|
} from '@solana/web3.js';
|
2020-08-13 08:34:25 -07:00
|
|
|
import { decodeEventQueue, decodeRequestQueue } from './queue';
|
2020-08-13 10:11:40 -07:00
|
|
|
import { Buffer } from 'buffer';
|
2020-08-07 01:38:49 -07:00
|
|
|
|
|
|
|
export const MARKET_STATE_LAYOUT = struct([
|
2020-08-12 12:58:29 -07:00
|
|
|
blob(5),
|
|
|
|
|
2020-08-13 07:24:51 -07:00
|
|
|
accountFlagsLayout('accountFlags'),
|
2020-08-07 01:38:49 -07:00
|
|
|
|
|
|
|
publicKeyLayout('ownAddress'),
|
|
|
|
|
|
|
|
u64('vaultSignerNonce'),
|
|
|
|
|
|
|
|
publicKeyLayout('baseMint'),
|
|
|
|
publicKeyLayout('quoteMint'),
|
|
|
|
|
|
|
|
publicKeyLayout('baseVault'),
|
|
|
|
u64('baseDepositsTotal'),
|
|
|
|
u64('baseFeesAccrued'),
|
|
|
|
|
|
|
|
publicKeyLayout('quoteVault'),
|
|
|
|
u64('quoteDepositsTotal'),
|
|
|
|
u64('quoteFeesAccrued'),
|
|
|
|
|
|
|
|
u64('quoteDustThreshold'),
|
|
|
|
|
|
|
|
publicKeyLayout('requestQueue'),
|
|
|
|
publicKeyLayout('eventQueue'),
|
|
|
|
|
|
|
|
publicKeyLayout('bids'),
|
|
|
|
publicKeyLayout('asks'),
|
|
|
|
|
|
|
|
u64('baseLotSize'),
|
|
|
|
u64('quoteLotSize'),
|
2020-08-12 06:37:05 -07:00
|
|
|
|
|
|
|
u64('feeRateBps'),
|
2020-08-12 12:58:29 -07:00
|
|
|
|
|
|
|
blob(7),
|
2020-08-07 01:38:49 -07:00
|
|
|
]);
|
|
|
|
|
2020-08-10 06:06:31 -07:00
|
|
|
export class Market {
|
2020-08-13 10:11:40 -07:00
|
|
|
private _decoded: any;
|
|
|
|
private _baseSplTokenDecimals: number;
|
|
|
|
private _quoteSplTokenDecimals: number;
|
2020-08-18 03:16:08 -07:00
|
|
|
private _skipPreflight: boolean;
|
|
|
|
private _confirmations: number;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
decoded,
|
|
|
|
baseMintDecimals: number,
|
|
|
|
quoteMintDecimals: number,
|
|
|
|
options: MarketOptions = {},
|
|
|
|
) {
|
|
|
|
const { skipPreflight = false, confirmations = 0 } = options;
|
2020-08-09 05:08:30 -07:00
|
|
|
if (!decoded.accountFlags.initialized || !decoded.accountFlags.market) {
|
|
|
|
throw new Error('Invalid market state');
|
|
|
|
}
|
|
|
|
this._decoded = decoded;
|
2020-08-13 03:52:41 -07:00
|
|
|
this._baseSplTokenDecimals = baseMintDecimals;
|
|
|
|
this._quoteSplTokenDecimals = quoteMintDecimals;
|
2020-08-18 03:16:08 -07:00
|
|
|
this._skipPreflight = skipPreflight;
|
|
|
|
this._confirmations = confirmations;
|
2020-08-09 05:08:30 -07:00
|
|
|
}
|
|
|
|
|
2020-08-12 11:45:42 -07:00
|
|
|
static get LAYOUT() {
|
|
|
|
return MARKET_STATE_LAYOUT;
|
2020-08-09 05:08:30 -07:00
|
|
|
}
|
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
static async load(
|
|
|
|
connection: Connection,
|
|
|
|
address: PublicKey,
|
|
|
|
options: MarketOptions = {},
|
|
|
|
) {
|
2020-08-13 10:11:40 -07:00
|
|
|
const { owner, data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(address),
|
|
|
|
'Market not found',
|
|
|
|
);
|
2020-08-09 05:08:30 -07:00
|
|
|
if (!owner.equals(DEX_PROGRAM_ID)) {
|
|
|
|
throw new Error('Address not owned by program');
|
|
|
|
}
|
2020-08-10 09:26:22 -07:00
|
|
|
const decoded = MARKET_STATE_LAYOUT.decode(data);
|
2020-08-12 11:45:42 -07:00
|
|
|
if (
|
|
|
|
!decoded.accountFlags.initialized ||
|
|
|
|
!decoded.accountFlags.market ||
|
|
|
|
!decoded.ownAddress.equals(address)
|
|
|
|
) {
|
|
|
|
throw new Error('Invalid market');
|
|
|
|
}
|
2020-08-12 06:37:05 -07:00
|
|
|
const [baseMintDecimals, quoteMintDecimals] = await Promise.all([
|
|
|
|
getMintDecimals(connection, decoded.baseMint),
|
|
|
|
getMintDecimals(connection, decoded.quoteMint),
|
2020-08-10 09:26:22 -07:00
|
|
|
]);
|
2020-08-18 03:16:08 -07:00
|
|
|
return new Market(decoded, baseMintDecimals, quoteMintDecimals, options);
|
2020-08-09 05:08:30 -07:00
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
get address(): PublicKey {
|
2020-08-12 11:45:42 -07:00
|
|
|
return this._decoded.ownAddress;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
get publicKey(): PublicKey {
|
2020-08-12 11:45:42 -07:00
|
|
|
return this.address;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
get baseMintAddress(): PublicKey {
|
2020-08-12 11:45:42 -07:00
|
|
|
return this._decoded.baseMint;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
get quoteMintAddress(): PublicKey {
|
2020-08-12 11:45:42 -07:00
|
|
|
return this._decoded.quoteMint;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async loadBids(connection: Connection): Promise<Orderbook> {
|
|
|
|
const { data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(this._decoded.bids),
|
|
|
|
);
|
2020-08-09 05:08:30 -07:00
|
|
|
return Orderbook.decode(this, data);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async loadAsks(connection: Connection): Promise<Orderbook> {
|
|
|
|
const { data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(this._decoded.asks),
|
|
|
|
);
|
2020-08-09 05:08:30 -07:00
|
|
|
return Orderbook.decode(this, data);
|
|
|
|
}
|
|
|
|
|
2020-08-13 13:32:51 -07:00
|
|
|
async loadOrdersForOwner(connection: Connection, ownerAddress: PublicKey) {
|
2020-08-16 23:51:39 -07:00
|
|
|
const [bids, asks, openOrdersAccounts] = await Promise.all([
|
2020-08-13 13:32:51 -07:00
|
|
|
this.loadBids(connection),
|
|
|
|
this.loadAsks(connection),
|
2020-08-16 23:51:39 -07:00
|
|
|
this.findOpenOrdersAccountsForOwner(connection, ownerAddress),
|
2020-08-13 13:32:51 -07:00
|
|
|
]);
|
2020-08-24 15:30:44 -07:00
|
|
|
const orders = [...bids, ...asks].filter((order) =>
|
2020-08-16 23:51:39 -07:00
|
|
|
openOrdersAccounts.some((openOrders) =>
|
2020-08-18 03:16:08 -07:00
|
|
|
order.openOrdersAddress.equals(openOrders.address),
|
2020-08-16 23:51:39 -07:00
|
|
|
),
|
2020-08-13 13:32:51 -07:00
|
|
|
);
|
2020-08-24 15:30:44 -07:00
|
|
|
// return orders;
|
|
|
|
return orders.map((order) => ({
|
|
|
|
...order,
|
|
|
|
clientId: openOrdersAccounts.find((openOrders) =>
|
|
|
|
order.openOrdersAddress.equals(openOrders.address),
|
|
|
|
)?.clientIds[order.openOrdersSlot],
|
|
|
|
}));
|
2020-08-13 13:32:51 -07:00
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async findBaseTokenAccountsForOwner(
|
|
|
|
connection: Connection,
|
|
|
|
ownerAddress: PublicKey,
|
|
|
|
): Promise<Array<{ pubkey: PublicKey; account: AccountInfo<Buffer> }>> {
|
2020-08-12 11:45:42 -07:00
|
|
|
return (
|
|
|
|
await connection.getTokenAccountsByOwner(ownerAddress, {
|
|
|
|
mint: this.baseMintAddress,
|
|
|
|
})
|
|
|
|
).value;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async findQuoteTokenAccountsForOwner(
|
|
|
|
connection: Connection,
|
|
|
|
ownerAddress: PublicKey,
|
|
|
|
): Promise<{ pubkey: PublicKey; account: AccountInfo<Buffer> }[]> {
|
2020-08-12 11:45:42 -07:00
|
|
|
return (
|
|
|
|
await connection.getTokenAccountsByOwner(ownerAddress, {
|
|
|
|
mint: this.quoteMintAddress,
|
|
|
|
})
|
|
|
|
).value;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async findOpenOrdersAccountsForOwner(
|
|
|
|
connection: Connection,
|
|
|
|
ownerAddress: PublicKey,
|
|
|
|
): Promise<OpenOrders[]> {
|
2020-08-12 11:45:42 -07:00
|
|
|
return OpenOrders.findForMarketAndOwner(
|
|
|
|
connection,
|
|
|
|
this.address,
|
|
|
|
ownerAddress,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-10 09:26:22 -07:00
|
|
|
async placeOrder(
|
2020-08-13 10:11:40 -07:00
|
|
|
connection: Connection,
|
2020-08-24 15:30:44 -07:00
|
|
|
{
|
|
|
|
owner,
|
|
|
|
payer,
|
|
|
|
side,
|
|
|
|
price,
|
|
|
|
size,
|
|
|
|
orderType = 'limit',
|
|
|
|
clientId,
|
|
|
|
}: OrderParams,
|
2020-08-10 09:26:22 -07:00
|
|
|
) {
|
2020-08-12 11:45:42 -07:00
|
|
|
const { transaction, signers } = await this.makePlaceOrderTransaction(
|
|
|
|
connection,
|
|
|
|
{
|
|
|
|
owner,
|
|
|
|
payer,
|
|
|
|
side,
|
|
|
|
price,
|
|
|
|
size,
|
|
|
|
orderType,
|
2020-08-24 15:30:44 -07:00
|
|
|
clientId,
|
2020-08-12 11:45:42 -07:00
|
|
|
},
|
|
|
|
);
|
2020-08-18 03:16:08 -07:00
|
|
|
return await this._sendTransaction(connection, transaction, signers);
|
2020-08-12 11:45:42 -07:00
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async makePlaceOrderTransaction<T extends PublicKey | Account>(
|
|
|
|
connection: Connection,
|
2020-08-24 15:30:44 -07:00
|
|
|
{
|
|
|
|
owner,
|
|
|
|
payer,
|
|
|
|
side,
|
|
|
|
price,
|
|
|
|
size,
|
|
|
|
orderType = 'limit',
|
|
|
|
clientId,
|
|
|
|
}: OrderParams<T>,
|
2020-08-12 11:45:42 -07:00
|
|
|
) {
|
2020-08-13 10:11:40 -07:00
|
|
|
// @ts-ignore
|
|
|
|
const ownerAddress: PublicKey = owner.publicKey ?? owner;
|
2020-08-12 11:45:42 -07:00
|
|
|
const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner(
|
|
|
|
connection,
|
|
|
|
ownerAddress,
|
|
|
|
); // TODO: cache this
|
|
|
|
const transaction = new Transaction();
|
2020-08-13 10:11:40 -07:00
|
|
|
const signers: (T | Account)[] = [owner];
|
2020-08-12 11:45:42 -07:00
|
|
|
let openOrdersAddress;
|
|
|
|
if (openOrdersAccounts.length === 0) {
|
|
|
|
const newOpenOrdersAccount = new Account();
|
|
|
|
transaction.add(
|
|
|
|
await OpenOrders.makeCreateAccountTransaction(
|
|
|
|
connection,
|
|
|
|
this.address,
|
|
|
|
ownerAddress,
|
|
|
|
newOpenOrdersAccount.publicKey,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
openOrdersAddress = newOpenOrdersAccount.publicKey;
|
|
|
|
signers.push(newOpenOrdersAccount);
|
|
|
|
} else {
|
|
|
|
openOrdersAddress = openOrdersAccounts[0].address;
|
|
|
|
}
|
2020-08-15 22:27:50 -07:00
|
|
|
if (this.baseSizeNumberToLots(size).lte(new BN(0))) {
|
|
|
|
throw new Error('size too small');
|
|
|
|
}
|
|
|
|
if (this.priceNumberToLots(price).lte(new BN(0))) {
|
|
|
|
throw new Error('invalid price');
|
|
|
|
}
|
2020-08-12 11:45:42 -07:00
|
|
|
transaction.add(
|
|
|
|
DexInstructions.newOrder({
|
|
|
|
market: this.address,
|
|
|
|
requestQueue: this._decoded.requestQueue,
|
|
|
|
baseVault: this._decoded.baseVault,
|
|
|
|
quoteVault: this._decoded.quoteVault,
|
|
|
|
openOrders: openOrdersAddress,
|
|
|
|
owner: ownerAddress,
|
|
|
|
payer,
|
|
|
|
side,
|
|
|
|
limitPrice: this.priceNumberToLots(price),
|
|
|
|
maxQuantity: this.baseSizeNumberToLots(size),
|
|
|
|
orderType,
|
2020-08-24 15:30:44 -07:00
|
|
|
clientId,
|
2020-08-12 11:45:42 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
return { transaction, signers };
|
2020-08-10 09:26:22 -07:00
|
|
|
}
|
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
private async _sendTransaction(
|
|
|
|
connection: Connection,
|
|
|
|
transaction: Transaction,
|
|
|
|
signers: Array<Account>,
|
|
|
|
): Promise<TransactionSignature> {
|
|
|
|
const signature = await connection.sendTransaction(transaction, signers, {
|
|
|
|
skipPreflight: this._skipPreflight,
|
|
|
|
});
|
|
|
|
if (this._confirmations > 0) {
|
2020-08-18 04:23:24 -07:00
|
|
|
const { value } = await connection.confirmTransaction(
|
|
|
|
signature,
|
|
|
|
this._confirmations,
|
|
|
|
);
|
|
|
|
if (value?.err) {
|
|
|
|
throw new Error(JSON.stringify(value.err));
|
|
|
|
}
|
2020-08-18 03:16:08 -07:00
|
|
|
}
|
|
|
|
return signature;
|
|
|
|
}
|
|
|
|
|
2020-08-24 15:30:44 -07:00
|
|
|
async cancelOrderByClientId(
|
|
|
|
connection: Connection,
|
|
|
|
owner: Account,
|
|
|
|
openOrders: PublicKey,
|
|
|
|
clientId: BN,
|
|
|
|
) {
|
|
|
|
const transaction = await this.makeCancelOrderByClientIdTransaction(
|
|
|
|
connection,
|
|
|
|
owner.publicKey,
|
|
|
|
openOrders,
|
|
|
|
clientId,
|
|
|
|
);
|
|
|
|
return await this._sendTransaction(connection, transaction, [owner]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async makeCancelOrderByClientIdTransaction(
|
|
|
|
connection: Connection,
|
|
|
|
owner: PublicKey,
|
|
|
|
openOrders: PublicKey,
|
|
|
|
clientId: BN,
|
|
|
|
) {
|
|
|
|
const transaction = new Transaction();
|
|
|
|
transaction.add(
|
|
|
|
DexInstructions.cancelOrderByClientId({
|
|
|
|
market: this.address,
|
|
|
|
owner,
|
|
|
|
openOrders,
|
|
|
|
requestQueue: this._decoded.requestQueue,
|
|
|
|
clientId,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
return transaction;
|
|
|
|
}
|
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
async cancelOrder(connection: Connection, owner: Account, order: Order) {
|
2020-08-13 03:52:41 -07:00
|
|
|
const transaction = await this.makeCancelOrderTransaction(
|
|
|
|
connection,
|
2020-08-18 03:16:08 -07:00
|
|
|
owner.publicKey,
|
2020-08-13 03:52:41 -07:00
|
|
|
order,
|
|
|
|
);
|
2020-08-18 03:16:08 -07:00
|
|
|
return await this._sendTransaction(connection, transaction, [owner]);
|
2020-08-13 03:52:41 -07:00
|
|
|
}
|
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
async makeCancelOrderTransaction(
|
|
|
|
connection: Connection,
|
|
|
|
owner: PublicKey,
|
|
|
|
order: Order,
|
|
|
|
) {
|
2020-08-13 03:52:41 -07:00
|
|
|
const transaction = new Transaction();
|
|
|
|
transaction.add(
|
|
|
|
DexInstructions.cancelOrder({
|
|
|
|
market: this.address,
|
2020-08-18 03:16:08 -07:00
|
|
|
owner,
|
|
|
|
openOrders: order.openOrdersAddress,
|
2020-08-13 03:52:41 -07:00
|
|
|
requestQueue: this._decoded.requestQueue,
|
|
|
|
side: order.side,
|
|
|
|
orderId: order.orderId,
|
2020-08-18 03:16:08 -07:00
|
|
|
openOrdersSlot: order.openOrdersSlot,
|
2020-08-13 03:52:41 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
return transaction;
|
|
|
|
}
|
|
|
|
|
2020-08-18 04:23:24 -07:00
|
|
|
async settleFunds(
|
|
|
|
connection: Connection,
|
|
|
|
owner: Account,
|
|
|
|
openOrders: OpenOrders,
|
|
|
|
baseWallet: PublicKey,
|
|
|
|
quoteWallet: PublicKey,
|
|
|
|
) {
|
|
|
|
if (!openOrders.owner.equals(owner.publicKey)) {
|
|
|
|
throw new Error('Invalid open orders account');
|
|
|
|
}
|
|
|
|
const transaction = await this.makeSettleFundsTransaction(
|
|
|
|
connection,
|
|
|
|
openOrders,
|
|
|
|
baseWallet,
|
|
|
|
quoteWallet,
|
|
|
|
);
|
|
|
|
return await this._sendTransaction(connection, transaction, [owner]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async makeSettleFundsTransaction(
|
|
|
|
connection: Connection,
|
|
|
|
openOrders: OpenOrders,
|
|
|
|
baseWallet: PublicKey,
|
|
|
|
quoteWallet: PublicKey,
|
|
|
|
) {
|
|
|
|
const tx = new Transaction();
|
|
|
|
// @ts-ignore
|
|
|
|
const vaultSigner = await PublicKey.createProgramAddress(
|
|
|
|
[
|
|
|
|
this.address.toBuffer(),
|
|
|
|
this._decoded.vaultSignerNonce.toArrayLike(Buffer, 'le', 8),
|
|
|
|
],
|
|
|
|
DEX_PROGRAM_ID,
|
|
|
|
);
|
|
|
|
tx.add(
|
|
|
|
DexInstructions.settleFunds({
|
|
|
|
market: this.address,
|
|
|
|
openOrders: openOrders.address,
|
|
|
|
owner: openOrders.owner,
|
|
|
|
baseVault: this._decoded.baseVault,
|
|
|
|
quoteVault: this._decoded.quoteVault,
|
|
|
|
baseWallet,
|
|
|
|
quoteWallet,
|
|
|
|
vaultSigner,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
return tx;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async loadRequestQueue(connection: Connection) {
|
|
|
|
const { data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(this._decoded.requestQueue),
|
2020-08-13 08:34:25 -07:00
|
|
|
);
|
|
|
|
return decodeRequestQueue(data);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async loadEventQueue(connection: Connection) {
|
|
|
|
const { data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(this._decoded.eventQueue),
|
|
|
|
);
|
2020-08-13 08:34:25 -07:00
|
|
|
return decodeEventQueue(data);
|
|
|
|
}
|
|
|
|
|
2020-08-13 12:45:16 -07:00
|
|
|
async loadFills(connection: Connection, limit = 100) {
|
|
|
|
// TODO: once there's a separate source of fills use that instead
|
|
|
|
const { data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(this._decoded.eventQueue),
|
|
|
|
);
|
|
|
|
const events = decodeEventQueue(data, limit);
|
|
|
|
return events
|
2020-08-24 15:30:44 -07:00
|
|
|
.filter(
|
|
|
|
(event) => event.eventFlags.fill && event.nativeQuantityPaid.gtn(0),
|
|
|
|
)
|
2020-08-13 12:45:16 -07:00
|
|
|
.map((event) =>
|
|
|
|
event.eventFlags.bid
|
|
|
|
? {
|
|
|
|
...event,
|
2020-08-24 15:30:44 -07:00
|
|
|
size: this.baseSplSizeToNumber(event.nativeQuantityReleased),
|
|
|
|
price: event.nativeQuantityPaid
|
|
|
|
.mul(this._quoteSplTokenMultiplier)
|
|
|
|
.divRound(
|
|
|
|
event.nativeQuantityReleased.mul(
|
|
|
|
this._baseSplTokenMultiplier,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toNumber(),
|
2020-08-13 12:45:16 -07:00
|
|
|
side: 'buy',
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
...event,
|
2020-08-24 15:30:44 -07:00
|
|
|
size: this.baseSplSizeToNumber(event.nativeQuantityPaid),
|
|
|
|
price: event.nativeQuantityReleased
|
|
|
|
.mul(this._quoteSplTokenMultiplier)
|
|
|
|
.divRound(
|
|
|
|
event.nativeQuantityPaid.mul(this._baseSplTokenMultiplier),
|
|
|
|
)
|
|
|
|
.toNumber(),
|
2020-08-13 12:45:16 -07:00
|
|
|
side: 'sell',
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
private get _baseSplTokenMultiplier() {
|
2020-08-13 03:52:41 -07:00
|
|
|
return new BN(10).pow(new BN(this._baseSplTokenDecimals));
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
private get _quoteSplTokenMultiplier() {
|
2020-08-13 03:52:41 -07:00
|
|
|
return new BN(10).pow(new BN(this._quoteSplTokenDecimals));
|
2020-08-10 09:26:22 -07:00
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
priceLotsToNumber(price: BN) {
|
2020-08-09 05:08:30 -07:00
|
|
|
return divideBnToNumber(
|
2020-08-13 03:52:41 -07:00
|
|
|
price.mul(this._decoded.quoteLotSize).mul(this._baseSplTokenMultiplier),
|
|
|
|
this._decoded.baseLotSize.mul(this._quoteSplTokenMultiplier),
|
2020-08-09 05:08:30 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
priceNumberToLots(price: number): BN {
|
2020-08-12 11:45:42 -07:00
|
|
|
return new BN(
|
|
|
|
Math.round(
|
|
|
|
(price *
|
2020-08-13 03:52:41 -07:00
|
|
|
Math.pow(10, this._quoteSplTokenDecimals) *
|
2020-08-12 11:45:42 -07:00
|
|
|
this._decoded.baseLotSize.toNumber()) /
|
2020-08-13 03:52:41 -07:00
|
|
|
(Math.pow(10, this._baseSplTokenDecimals) *
|
2020-08-12 11:45:42 -07:00
|
|
|
this._decoded.quoteLotSize.toNumber()),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-24 15:30:44 -07:00
|
|
|
baseSplSizeToNumber(size: BN) {
|
|
|
|
return divideBnToNumber(size, this._baseSplTokenMultiplier);
|
|
|
|
}
|
|
|
|
|
|
|
|
quoteSplSizeToNumber(size: BN) {
|
|
|
|
return divideBnToNumber(size, this._quoteSplTokenMultiplier);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
baseSizeLotsToNumber(size: BN) {
|
2020-08-09 05:08:30 -07:00
|
|
|
return divideBnToNumber(
|
|
|
|
size.mul(this._decoded.baseLotSize),
|
2020-08-13 03:52:41 -07:00
|
|
|
this._baseSplTokenMultiplier,
|
2020-08-09 05:08:30 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
baseSizeNumberToLots(size: number): BN {
|
2020-08-12 11:45:42 -07:00
|
|
|
const native = new BN(
|
2020-08-13 03:52:41 -07:00
|
|
|
Math.round(size * Math.pow(10, this._baseSplTokenDecimals)),
|
2020-08-12 11:45:42 -07:00
|
|
|
);
|
|
|
|
// rounds down to the nearest lot size
|
|
|
|
return native.div(this._decoded.baseLotSize);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
quoteSizeLotsToNumber(size: BN) {
|
2020-08-09 05:08:30 -07:00
|
|
|
return divideBnToNumber(
|
|
|
|
size.mul(this._decoded.quoteLotSize),
|
2020-08-13 03:52:41 -07:00
|
|
|
this._quoteSplTokenMultiplier,
|
2020-08-09 05:08:30 -07:00
|
|
|
);
|
|
|
|
}
|
2020-08-12 11:45:42 -07:00
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
quoteSizeNumberToLots(size: number): BN {
|
2020-08-12 11:45:42 -07:00
|
|
|
const native = new BN(
|
2020-08-13 03:52:41 -07:00
|
|
|
Math.round(size * Math.pow(10, this._quoteSplTokenDecimals)),
|
2020-08-12 11:45:42 -07:00
|
|
|
);
|
|
|
|
// rounds down to the nearest lot size
|
|
|
|
return native.div(this._decoded.quoteLotSize);
|
|
|
|
}
|
2020-08-13 03:52:41 -07:00
|
|
|
|
2020-08-15 22:27:50 -07:00
|
|
|
get minOrderSize() {
|
|
|
|
return this.baseSizeLotsToNumber(new BN(1));
|
|
|
|
}
|
|
|
|
|
|
|
|
get tickSize() {
|
|
|
|
return this.priceLotsToNumber(new BN(1));
|
|
|
|
}
|
|
|
|
|
2020-08-22 23:13:20 -07:00
|
|
|
makeMatchOrdersInstruction(limit: number): TransactionInstruction {
|
|
|
|
return DexInstructions.matchOrders({
|
|
|
|
market: this.address,
|
|
|
|
requestQueue: this._decoded.requestQueue,
|
|
|
|
eventQueue: this._decoded.eventQueue,
|
|
|
|
bids: this._decoded.bids,
|
|
|
|
asks: this._decoded.asks,
|
|
|
|
baseVault: this._decoded.baseVault,
|
|
|
|
quoteVault: this._decoded.quoteVault,
|
|
|
|
limit,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async matchOrders(connection: Connection, feePayer: Account, limit: number) {
|
2020-08-13 03:52:41 -07:00
|
|
|
const tx = new Transaction();
|
2020-08-22 23:13:20 -07:00
|
|
|
tx.add(this.makeMatchOrdersInstruction(limit));
|
2020-08-18 03:16:08 -07:00
|
|
|
return await this._sendTransaction(connection, tx, [feePayer]);
|
2020-08-13 03:52:41 -07:00
|
|
|
}
|
2020-08-09 05:08:30 -07:00
|
|
|
}
|
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
export interface MarketOptions {
|
|
|
|
skipPreflight?: boolean;
|
|
|
|
confirmations?: number;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
export interface OrderParams<T = Account> {
|
|
|
|
owner: T;
|
|
|
|
payer: PublicKey;
|
|
|
|
side: 'buy' | 'sell';
|
|
|
|
price: number;
|
|
|
|
size: number;
|
|
|
|
orderType?: 'limit' | 'ioc' | 'postOnly';
|
2020-08-24 15:30:44 -07:00
|
|
|
clientId?: BN;
|
2020-08-13 10:11:40 -07:00
|
|
|
}
|
|
|
|
|
2020-08-07 01:38:49 -07:00
|
|
|
export const OPEN_ORDERS_LAYOUT = struct([
|
2020-08-12 12:58:29 -07:00
|
|
|
blob(5),
|
|
|
|
|
2020-08-13 07:24:51 -07:00
|
|
|
accountFlagsLayout('accountFlags'),
|
2020-08-07 01:38:49 -07:00
|
|
|
|
|
|
|
publicKeyLayout('market'),
|
|
|
|
publicKeyLayout('owner'),
|
|
|
|
|
2020-08-13 03:52:41 -07:00
|
|
|
// These are in spl-token (i.e. not lot) units
|
|
|
|
u64('baseTokenFree'),
|
|
|
|
u64('baseTokenTotal'),
|
|
|
|
u64('quoteTokenFree'),
|
|
|
|
u64('quoteTokenTotal'),
|
2020-08-07 01:38:49 -07:00
|
|
|
|
|
|
|
u128('freeSlotBits'),
|
|
|
|
u128('isBidBits'),
|
|
|
|
|
|
|
|
seq(u128(), 128, 'orders'),
|
2020-08-24 15:30:44 -07:00
|
|
|
seq(u64(), 128, 'clientIds'),
|
2020-08-12 12:58:29 -07:00
|
|
|
|
|
|
|
blob(7),
|
2020-08-07 01:38:49 -07:00
|
|
|
]);
|
|
|
|
|
2020-08-12 11:45:42 -07:00
|
|
|
export class OpenOrders {
|
2020-08-13 10:11:40 -07:00
|
|
|
address: PublicKey;
|
|
|
|
market!: PublicKey;
|
|
|
|
owner!: PublicKey;
|
|
|
|
|
|
|
|
baseTokenFree!: BN;
|
|
|
|
baseTokenTotal!: BN;
|
|
|
|
quoteTokenFree!: BN;
|
|
|
|
quoteTokenTotal!: BN;
|
|
|
|
|
|
|
|
orders!: BN[];
|
2020-08-24 15:30:44 -07:00
|
|
|
clientIds!: BN[];
|
2020-08-13 10:11:40 -07:00
|
|
|
|
|
|
|
constructor(address: PublicKey, decoded) {
|
2020-08-12 11:45:42 -07:00
|
|
|
this.address = address;
|
|
|
|
Object.assign(this, decoded);
|
|
|
|
}
|
|
|
|
|
|
|
|
static get LAYOUT() {
|
|
|
|
return OPEN_ORDERS_LAYOUT;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
static async findForMarketAndOwner(
|
|
|
|
connection: Connection,
|
|
|
|
marketAddress: PublicKey,
|
|
|
|
ownerAddress: PublicKey,
|
|
|
|
) {
|
2020-08-12 11:45:42 -07:00
|
|
|
const filters = [
|
|
|
|
{
|
|
|
|
memcmp: {
|
|
|
|
offset: OPEN_ORDERS_LAYOUT.offsetOf('market'),
|
|
|
|
bytes: marketAddress.toBase58(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
memcmp: {
|
|
|
|
offset: OPEN_ORDERS_LAYOUT.offsetOf('owner'),
|
|
|
|
bytes: ownerAddress.toBase58(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
dataSize: OPEN_ORDERS_LAYOUT.span,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
const accounts = await getFilteredProgramAccounts(
|
|
|
|
connection,
|
|
|
|
DEX_PROGRAM_ID,
|
|
|
|
filters,
|
|
|
|
);
|
|
|
|
return accounts.map(({ publicKey, accountInfo }) =>
|
|
|
|
OpenOrders.fromAccountInfo(publicKey, accountInfo),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
static async load(connection: Connection, address: PublicKey) {
|
2020-08-12 11:45:42 -07:00
|
|
|
const accountInfo = await connection.getAccountInfo(address);
|
|
|
|
if (accountInfo === null) {
|
|
|
|
throw new Error('Open orders account not found');
|
|
|
|
}
|
2020-08-13 10:11:40 -07:00
|
|
|
return OpenOrders.fromAccountInfo(address, accountInfo);
|
2020-08-12 11:45:42 -07:00
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
static fromAccountInfo(address: PublicKey, accountInfo: AccountInfo<Buffer>) {
|
2020-08-12 11:45:42 -07:00
|
|
|
const { owner, data } = accountInfo;
|
|
|
|
if (!owner.equals(DEX_PROGRAM_ID)) {
|
|
|
|
throw new Error('Address not owned by program');
|
|
|
|
}
|
|
|
|
const decoded = OPEN_ORDERS_LAYOUT.decode(data);
|
|
|
|
if (!decoded.accountFlags.initialized || !decoded.accountFlags.openOrders) {
|
|
|
|
throw new Error('Invalid open orders account');
|
|
|
|
}
|
|
|
|
return new OpenOrders(address, decoded);
|
|
|
|
}
|
|
|
|
|
|
|
|
static async makeCreateAccountTransaction(
|
2020-08-13 10:11:40 -07:00
|
|
|
connection: Connection,
|
|
|
|
marketAddress: PublicKey,
|
|
|
|
ownerAddress: PublicKey,
|
|
|
|
newAccountAddress: PublicKey,
|
2020-08-12 11:45:42 -07:00
|
|
|
) {
|
|
|
|
return SystemProgram.createAccount({
|
|
|
|
fromPubkey: ownerAddress,
|
|
|
|
newAccountPubkey: newAccountAddress,
|
|
|
|
lamports: await connection.getMinimumBalanceForRentExemption(
|
|
|
|
OPEN_ORDERS_LAYOUT.span,
|
|
|
|
),
|
|
|
|
space: OPEN_ORDERS_LAYOUT.span,
|
|
|
|
programId: DEX_PROGRAM_ID,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get publicKey() {
|
|
|
|
return this.address;
|
|
|
|
}
|
|
|
|
}
|
2020-08-10 06:06:31 -07:00
|
|
|
|
2020-08-07 01:38:49 -07:00
|
|
|
export const ORDERBOOK_LAYOUT = struct([
|
2020-08-12 12:58:29 -07:00
|
|
|
blob(5),
|
2020-08-13 07:24:51 -07:00
|
|
|
accountFlagsLayout('accountFlags'),
|
2020-08-07 01:38:49 -07:00
|
|
|
SLAB_LAYOUT.replicate('slab'),
|
2020-08-12 12:58:29 -07:00
|
|
|
blob(7),
|
2020-08-07 01:38:49 -07:00
|
|
|
]);
|
2020-08-09 05:08:30 -07:00
|
|
|
|
|
|
|
export class Orderbook {
|
2020-08-13 10:11:40 -07:00
|
|
|
market: Market;
|
|
|
|
isBids: boolean;
|
|
|
|
slab: Slab;
|
|
|
|
|
|
|
|
constructor(market: Market, accountFlags, slab: Slab) {
|
2020-08-09 05:08:30 -07:00
|
|
|
if (!accountFlags.initialized || !(accountFlags.bids ^ accountFlags.asks)) {
|
|
|
|
throw new Error('Invalid orderbook');
|
|
|
|
}
|
|
|
|
this.market = market;
|
|
|
|
this.isBids = accountFlags.bids;
|
|
|
|
this.slab = slab;
|
|
|
|
}
|
|
|
|
|
2020-08-13 13:32:51 -07:00
|
|
|
static get LAYOUT() {
|
|
|
|
return ORDERBOOK_LAYOUT;
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
static decode(market: Market, buffer: Buffer) {
|
2020-08-09 05:08:30 -07:00
|
|
|
const { accountFlags, slab } = ORDERBOOK_LAYOUT.decode(buffer);
|
|
|
|
return new Orderbook(market, accountFlags, slab);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
getL2(depth: number): [number, number, BN, BN][] {
|
2020-08-09 05:08:30 -07:00
|
|
|
const descending = this.isBids;
|
2020-08-13 10:11:40 -07:00
|
|
|
const levels: [BN, BN][] = []; // (price, size)
|
2020-08-10 06:06:31 -07:00
|
|
|
for (const { key, quantity } of this.slab.items(descending)) {
|
2020-08-09 05:08:30 -07:00
|
|
|
const price = getPriceFromKey(key);
|
2020-08-13 10:11:40 -07:00
|
|
|
if (levels.length > 0 && levels[levels.length - 1][0].eq(price)) {
|
2020-08-13 07:24:51 -07:00
|
|
|
levels[levels.length - 1][1].iadd(quantity);
|
2020-08-09 05:08:30 -07:00
|
|
|
} else if (levels.length === depth) {
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
levels.push([price, quantity]);
|
|
|
|
}
|
|
|
|
}
|
2020-08-12 11:45:42 -07:00
|
|
|
return levels.map(([priceLots, sizeLots]) => [
|
|
|
|
this.market.priceLotsToNumber(priceLots),
|
2020-08-13 10:11:40 -07:00
|
|
|
this.market.baseSizeLotsToNumber(sizeLots),
|
2020-08-12 11:45:42 -07:00
|
|
|
priceLots,
|
|
|
|
sizeLots,
|
2020-08-09 05:08:30 -07:00
|
|
|
]);
|
|
|
|
}
|
2020-08-10 09:26:22 -07:00
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
*[Symbol.iterator](): Generator<Order> {
|
2020-08-24 15:30:44 -07:00
|
|
|
for (const { key, ownerSlot, owner, quantity, feeTier } of this.slab) {
|
2020-08-10 09:26:22 -07:00
|
|
|
const price = getPriceFromKey(key);
|
|
|
|
yield {
|
|
|
|
orderId: key,
|
2020-08-18 03:16:08 -07:00
|
|
|
openOrdersAddress: owner,
|
|
|
|
openOrdersSlot: ownerSlot,
|
2020-08-24 15:30:44 -07:00
|
|
|
feeTier,
|
2020-08-12 11:45:42 -07:00
|
|
|
price: this.market.priceLotsToNumber(price),
|
|
|
|
priceLots: price,
|
|
|
|
size: this.market.baseSizeLotsToNumber(quantity),
|
|
|
|
sizeLots: quantity,
|
2020-08-13 13:32:51 -07:00
|
|
|
side: (this.isBids ? 'buy' : 'sell') as 'buy' | 'sell',
|
2020-08-10 09:26:22 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2020-08-09 05:08:30 -07:00
|
|
|
}
|
|
|
|
|
2020-08-18 03:16:08 -07:00
|
|
|
export interface Order {
|
|
|
|
orderId: BN;
|
|
|
|
openOrdersAddress: PublicKey;
|
|
|
|
openOrdersSlot: number;
|
|
|
|
price: number;
|
|
|
|
priceLots: BN;
|
|
|
|
size: number;
|
2020-08-24 15:30:44 -07:00
|
|
|
feeTier: number;
|
2020-08-18 03:16:08 -07:00
|
|
|
sizeLots: BN;
|
|
|
|
side: 'buy' | 'sell';
|
|
|
|
}
|
|
|
|
|
2020-08-09 05:08:30 -07:00
|
|
|
function getPriceFromKey(key) {
|
|
|
|
return key.ushrn(64);
|
|
|
|
}
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
function divideBnToNumber(numerator: BN, denominator: BN): number {
|
2020-08-09 05:08:30 -07:00
|
|
|
const quotient = numerator.div(denominator).toNumber();
|
|
|
|
const rem = numerator.umod(denominator);
|
|
|
|
const gcd = rem.gcd(denominator);
|
|
|
|
return quotient + rem.div(gcd).toNumber() / denominator.div(gcd).toNumber();
|
|
|
|
}
|
2020-08-10 09:26:22 -07:00
|
|
|
|
|
|
|
const MINT_LAYOUT = struct([blob(36), u8('decimals'), blob(3)]);
|
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
export async function getMintDecimals(
|
|
|
|
connection: Connection,
|
|
|
|
mint: PublicKey,
|
|
|
|
): Promise<number> {
|
|
|
|
const { data } = throwIfNull(
|
|
|
|
await connection.getAccountInfo(mint),
|
|
|
|
'mint not found',
|
|
|
|
);
|
2020-08-10 09:26:22 -07:00
|
|
|
const { decimals } = MINT_LAYOUT.decode(data);
|
|
|
|
return decimals;
|
|
|
|
}
|
2020-08-12 11:45:42 -07:00
|
|
|
|
2020-08-13 10:11:40 -07:00
|
|
|
async function getFilteredProgramAccounts(
|
|
|
|
connection: Connection,
|
|
|
|
programId: PublicKey,
|
|
|
|
filters,
|
|
|
|
): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }[]> {
|
|
|
|
// @ts-ignore
|
2020-08-12 11:45:42 -07:00
|
|
|
const resp = await connection._rpcRequest('getProgramAccounts', [
|
|
|
|
programId.toBase58(),
|
|
|
|
{
|
|
|
|
commitment: connection.commitment,
|
|
|
|
filters,
|
2020-08-20 09:57:21 -07:00
|
|
|
encoding: 'base64',
|
2020-08-12 11:45:42 -07:00
|
|
|
},
|
|
|
|
]);
|
|
|
|
if (resp.error) {
|
|
|
|
throw new Error(resp.error.message);
|
|
|
|
}
|
|
|
|
return resp.result.map(
|
|
|
|
({ pubkey, account: { data, executable, owner, lamports } }) => ({
|
|
|
|
publicKey: new PublicKey(pubkey),
|
|
|
|
accountInfo: {
|
2020-08-20 09:57:21 -07:00
|
|
|
data: Buffer.from(data[0], 'base64'),
|
2020-08-12 11:45:42 -07:00
|
|
|
executable,
|
|
|
|
owner: new PublicKey(owner),
|
|
|
|
lamports,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
2020-08-13 10:11:40 -07:00
|
|
|
|
|
|
|
function throwIfNull<T>(value: T | null, message = 'account not found'): T {
|
|
|
|
if (value === null) {
|
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|