@thespidercode/openbook-swap
Version:
Ready-to-use swap tool using Openbook DEX
1,819 lines (1,646 loc) • 53.6 kB
text/typescript
import {
Account,
AccountInfo,
Commitment,
Connection,
LAMPORTS_PER_SOL,
PublicKey,
SYSVAR_RENT_PUBKEY,
SystemProgram,
Transaction,
TransactionInstruction,
TransactionSignature
} from '@solana/web3.js';
import BN from 'bn.js';
import { Buffer } from 'buffer';
import { blob, seq, struct, u8 } from 'buffer-layout';
import { getFeeTier, supportsSrmFeeDiscounts } from './fees';
import { DexInstructions, INSTRUCTION_LAYOUT } from './instructions';
import { accountFlagsLayout, publicKeyLayout, u128, u64 } from './layout';
import { decodeEventQueue, decodeRequestQueue } from './queue';
import { Slab, SLAB_LAYOUT } from './slab';
import {
closeAccount,
initializeAccount,
MSRM_DECIMALS,
MSRM_MINT,
SRM_DECIMALS,
SRM_MINT,
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT
} from './token-instructions';
import { getLayoutVersion } from './tokens_and_markets';
export const _MARKET_STAT_LAYOUT_V1 = struct([
blob(5),
accountFlagsLayout('accountFlags'),
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'),
u64('feeRateBps'),
blob(7),
]);
export const MARKET_STATE_LAYOUT_V2 = struct([
blob(5),
accountFlagsLayout('accountFlags'),
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'),
u64('feeRateBps'),
u64('referrerRebatesAccrued'),
blob(7),
]);
export const MARKET_STATE_LAYOUT_V3 = struct([
blob(5),
accountFlagsLayout('accountFlags'),
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'),
u64('feeRateBps'),
u64('referrerRebatesAccrued'),
publicKeyLayout('authority'),
publicKeyLayout('pruneAuthority'),
publicKeyLayout('consumeEventsAuthority'),
blob(992),
blob(7),
]);
export class Market {
private _decoded: any;
private _baseSplTokenDecimals: number;
private _quoteSplTokenDecimals: number;
private _skipPreflight: boolean;
private _commitment: Commitment;
private _programId: PublicKey;
private _openOrdersAccountsCache: {
[publickKey: string]: { accounts: OpenOrders[]; ts: number };
};
private _layoutOverride?: any;
private _feeDiscountKeysCache: {
[publicKey: string]: {
accounts: Array<{
balance: number;
mint: PublicKey;
pubkey: PublicKey;
feeTier: number;
}>;
ts: number;
};
};
constructor(
decoded: any,
baseMintDecimals: number,
quoteMintDecimals: number,
options: MarketOptions = {},
programId: PublicKey,
layoutOverride?: any,
) {
const { skipPreflight = false, commitment = 'recent' } = options;
if (!decoded.accountFlags.initialized || !decoded.accountFlags.market) {
throw new Error('Invalid market state');
}
this._decoded = decoded;
this._baseSplTokenDecimals = baseMintDecimals;
this._quoteSplTokenDecimals = quoteMintDecimals;
this._skipPreflight = skipPreflight;
this._commitment = commitment;
this._programId = programId;
this._openOrdersAccountsCache = {};
this._feeDiscountKeysCache = {};
this._layoutOverride = layoutOverride;
}
static getLayout(programId: PublicKey) {
if (getLayoutVersion(programId) === 1) {
return _MARKET_STAT_LAYOUT_V1;
}
return MARKET_STATE_LAYOUT_V2;
}
static async findAccountsByMints(
connection: Connection,
baseMintAddress: PublicKey,
quoteMintAddress: PublicKey,
programId: PublicKey,
) {
const filters = [
{
memcmp: {
offset: this.getLayout(programId).offsetOf('baseMint'),
bytes: baseMintAddress.toBase58(),
},
},
{
memcmp: {
offset: Market.getLayout(programId).offsetOf('quoteMint'),
bytes: quoteMintAddress.toBase58(),
},
},
];
return getFilteredProgramAccounts(connection, programId, filters);
}
static async load(
connection: Connection,
address: PublicKey,
options: MarketOptions = {},
programId: PublicKey,
layoutOverride?: any,
) {
const { owner, data } = throwIfNull(
await connection.getAccountInfo(address),
'Market not found',
);
if (!owner.equals(programId)) {
throw new Error('Address not owned by program: ' + owner.toBase58());
}
const decoded = (layoutOverride ?? this.getLayout(programId)).decode(data);
if (
!decoded.accountFlags.initialized ||
!decoded.accountFlags.market ||
!decoded.ownAddress.equals(address)
) {
throw new Error('Invalid market');
}
const [baseMintDecimals, quoteMintDecimals] = await Promise.all([
getMintDecimals(connection, decoded.baseMint),
getMintDecimals(connection, decoded.quoteMint),
]);
return new Market(
decoded,
baseMintDecimals,
quoteMintDecimals,
options,
programId,
layoutOverride,
);
}
get programId(): PublicKey {
return this._programId;
}
get address(): PublicKey {
return this._decoded.ownAddress;
}
get publicKey(): PublicKey {
return this.address;
}
get baseMintAddress(): PublicKey {
return this._decoded.baseMint;
}
get quoteMintAddress(): PublicKey {
return this._decoded.quoteMint;
}
get bidsAddress(): PublicKey {
return this._decoded.bids;
}
get asksAddress(): PublicKey {
return this._decoded.asks;
}
get decoded(): any {
return this._decoded;
}
async loadBids(connection: Connection): Promise<Orderbook> {
const { data } = throwIfNull(
await connection.getAccountInfo(this._decoded.bids),
);
return Orderbook.decode(this, data);
}
async loadAsks(connection: Connection): Promise<Orderbook> {
const { data } = throwIfNull(
await connection.getAccountInfo(this._decoded.asks),
);
return Orderbook.decode(this, data);
}
async loadOrdersForOwner(
connection: Connection,
ownerAddress: PublicKey,
cacheDurationMs = 0,
): Promise<Order[]> {
const [bids, asks, openOrdersAccounts] = await Promise.all([
this.loadBids(connection),
this.loadAsks(connection),
this.findOpenOrdersAccountsForOwner(
connection,
ownerAddress,
cacheDurationMs,
),
]);
return this.filterForOpenOrders(bids, asks, openOrdersAccounts);
}
filterForOpenOrders(
bids: Orderbook,
asks: Orderbook,
openOrdersAccounts: OpenOrders[],
): Order[] {
return [...bids, ...asks].filter((order) =>
openOrdersAccounts.some((openOrders) =>
order.openOrdersAddress.equals(openOrders.address),
),
);
}
async findBaseTokenAccountsForOwner(
connection: Connection,
ownerAddress: PublicKey,
includeUnwrappedSol = false,
): Promise<Array<{ pubkey: PublicKey; account: AccountInfo<Buffer> }>> {
if (this.baseMintAddress.equals(WRAPPED_SOL_MINT) && includeUnwrappedSol) {
const [wrapped, unwrapped] = await Promise.all([
this.findBaseTokenAccountsForOwner(connection, ownerAddress, false),
connection.getAccountInfo(ownerAddress),
]);
if (unwrapped !== null) {
return [{ pubkey: ownerAddress, account: unwrapped }, ...wrapped];
}
return wrapped;
}
return await this.getTokenAccountsByOwnerForMint(
connection,
ownerAddress,
this.baseMintAddress,
);
}
async getTokenAccountsByOwnerForMint(
connection: Connection,
ownerAddress: PublicKey,
mintAddress: PublicKey,
): Promise<Array<{ pubkey: PublicKey; account: AccountInfo<Buffer> }>> {
const response: any = await connection.getTokenAccountsByOwner(ownerAddress, {
mint: mintAddress,
});
// Convert the response to a mutable array
const accounts: Array<{ pubkey: PublicKey; account: AccountInfo<Buffer> }> = [...response.value];
return accounts;
}
async findQuoteTokenAccountsForOwner(
connection: Connection,
ownerAddress: PublicKey,
includeUnwrappedSol = false,
): Promise<{ pubkey: PublicKey; account: AccountInfo<Buffer> }[]> {
if (this.quoteMintAddress.equals(WRAPPED_SOL_MINT) && includeUnwrappedSol) {
const [wrapped, unwrapped] = await Promise.all([
this.findQuoteTokenAccountsForOwner(connection, ownerAddress, false),
connection.getAccountInfo(ownerAddress),
]);
if (unwrapped !== null) {
return [{ pubkey: ownerAddress, account: unwrapped }, ...wrapped];
}
return wrapped;
}
return await this.getTokenAccountsByOwnerForMint(
connection,
ownerAddress,
this.quoteMintAddress,
);
}
async findOpenOrdersAccountsForOwner(
connection: Connection,
ownerAddress: PublicKey,
cacheDurationMs = 0,
): Promise<OpenOrders[]> {
const strOwner = ownerAddress.toBase58();
const now = new Date().getTime();
if (
strOwner in this._openOrdersAccountsCache &&
now - this._openOrdersAccountsCache[strOwner].ts < cacheDurationMs
) {
return this._openOrdersAccountsCache[strOwner].accounts;
}
const openOrdersAccountsForOwner = await OpenOrders.findForMarketAndOwner(
connection,
this.address,
ownerAddress,
this._programId,
);
this._openOrdersAccountsCache[strOwner] = {
accounts: openOrdersAccountsForOwner,
ts: now,
};
return openOrdersAccountsForOwner;
}
async replaceOrders(
connection: Connection,
accounts: OrderParamsAccounts,
orders: OrderParamsBase[],
cacheDurationMs = 0,
) {
if (!accounts.openOrdersAccount && !accounts.openOrdersAddressKey) {
const ownerAddress: PublicKey = accounts.owner.publicKey ?? accounts.owner;
const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner(
connection,
ownerAddress,
cacheDurationMs,
);
accounts.openOrdersAddressKey = openOrdersAccounts[0].address;
}
const transaction = new Transaction();
transaction.add(this.makeReplaceOrdersByClientIdsInstruction(accounts, orders));
return await this._sendTransaction(connection, transaction, [
accounts.owner,
]);
}
async placeOrder(
connection: Connection,
{
owner,
payer,
side,
price,
size,
orderType = 'limit',
clientId,
openOrdersAddressKey,
openOrdersAccount,
feeDiscountPubkey,
maxTs,
replaceIfExists = false,
}: OrderParams,
) {
const { transaction, signers } = await this.makePlaceOrderTransaction<
Account
>(connection, {
owner,
payer,
side,
price,
size,
orderType,
clientId,
openOrdersAddressKey,
openOrdersAccount,
feeDiscountPubkey,
maxTs,
replaceIfExists,
});
return await this._sendTransaction(connection, transaction, [
owner,
...signers,
]);
}
async sendTake(
connection: Connection,
{
owner,
baseWallet,
quoteWallet,
side,
price,
maxBaseSize,
maxQuoteSize,
minBaseSize,
minQuoteSize,
limit = 65535,
programId = undefined,
feeDiscountPubkey = undefined,
}: SendTakeParams,
) {
const { transaction, signers } = await this.makeSendTakeTransaction<Account>(
connection, {
owner,
baseWallet,
quoteWallet,
side,
price,
maxBaseSize,
maxQuoteSize,
minBaseSize,
minQuoteSize,
limit,
programId,
feeDiscountPubkey,
});
return await this._sendTransaction(connection, transaction, [
owner,
...signers
]);
}
getSplTokenBalanceFromAccountInfo(
accountInfo: AccountInfo<Buffer>,
decimals: number,
): number {
return divideBnToNumber(
new BN(accountInfo.data.slice(64, 72), 10, 'le'),
new BN(10).pow(new BN(decimals)),
);
}
get supportsSrmFeeDiscounts() {
return supportsSrmFeeDiscounts(this._programId);
}
get supportsReferralFees() {
return getLayoutVersion(this._programId) > 1;
}
get usesRequestQueue() {
return getLayoutVersion(this._programId) <= 2;
}
async findFeeDiscountKeys(
connection: Connection,
ownerAddress: PublicKey,
cacheDurationMs = 0,
): Promise<
Array<{
pubkey: PublicKey;
feeTier: number;
balance: number;
mint: PublicKey;
}>
> {
let sortedAccounts: Array<{
balance: number;
mint: PublicKey;
pubkey: PublicKey;
feeTier: number;
}> = [];
const now = new Date().getTime();
const strOwner = ownerAddress.toBase58();
if (
strOwner in this._feeDiscountKeysCache &&
now - this._feeDiscountKeysCache[strOwner].ts < cacheDurationMs
) {
return this._feeDiscountKeysCache[strOwner].accounts;
}
if (this.supportsSrmFeeDiscounts) {
// Fee discounts based on (M)SRM holdings supported in newer versions
const msrmAccounts = (
await this.getTokenAccountsByOwnerForMint(
connection,
ownerAddress,
MSRM_MINT,
)
).map(({ pubkey, account }) => {
const balance = this.getSplTokenBalanceFromAccountInfo(
account,
MSRM_DECIMALS,
);
return {
pubkey,
mint: MSRM_MINT,
balance,
feeTier: getFeeTier(balance, 0),
};
});
const srmAccounts = (
await this.getTokenAccountsByOwnerForMint(
connection,
ownerAddress,
SRM_MINT,
)
).map(({ pubkey, account }) => {
const balance = this.getSplTokenBalanceFromAccountInfo(
account,
SRM_DECIMALS,
);
return {
pubkey,
mint: SRM_MINT,
balance,
feeTier: getFeeTier(0, balance),
};
});
sortedAccounts = msrmAccounts.concat(srmAccounts).sort((a, b) => {
if (a.feeTier > b.feeTier) {
return -1;
} else if (a.feeTier < b.feeTier) {
return 1;
} else {
if (a.balance > b.balance) {
return -1;
} else if (a.balance < b.balance) {
return 1;
} else {
return 0;
}
}
});
}
this._feeDiscountKeysCache[strOwner] = {
accounts: sortedAccounts,
ts: now,
};
return sortedAccounts;
}
async findBestFeeDiscountKey(
connection: Connection,
ownerAddress: PublicKey,
cacheDurationMs = 30000,
): Promise<{ pubkey: PublicKey | null; feeTier: number }> {
const accounts = await this.findFeeDiscountKeys(
connection,
ownerAddress,
cacheDurationMs,
);
if (accounts.length > 0) {
return {
pubkey: accounts[0].pubkey,
feeTier: accounts[0].feeTier,
};
}
return {
pubkey: null,
feeTier: 0,
};
}
async makePlaceOrderTransaction<T extends PublicKey | Account>(
connection: Connection,
{
owner,
payer,
side,
price,
size,
orderType = 'limit',
clientId,
openOrdersAddressKey,
openOrdersAccount,
feeDiscountPubkey = undefined,
selfTradeBehavior = 'decrementTake',
maxTs,
replaceIfExists = false,
}: OrderParams<T>,
cacheDurationMs = 0,
feeDiscountPubkeyCacheDurationMs = 0,
) {
// @ts-ignore
const ownerAddress: PublicKey = owner.publicKey ?? owner;
const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner(
connection,
ownerAddress,
cacheDurationMs,
);
const transaction = new Transaction();
const signers: Account[] = [];
// Fetch an SRM fee discount key if the market supports discounts and it is not supplied
let useFeeDiscountPubkey: PublicKey | null;
if (feeDiscountPubkey) {
useFeeDiscountPubkey = feeDiscountPubkey;
} else if (
feeDiscountPubkey === undefined &&
this.supportsSrmFeeDiscounts
) {
useFeeDiscountPubkey = (
await this.findBestFeeDiscountKey(
connection,
ownerAddress,
feeDiscountPubkeyCacheDurationMs,
)
).pubkey;
} else {
useFeeDiscountPubkey = null;
}
let openOrdersAddress: PublicKey;
if (openOrdersAccounts.length === 0) {
let account;
if (openOrdersAccount) {
account = openOrdersAccount;
} else {
account = new Account();
}
transaction.add(
await OpenOrders.makeCreateAccountTransaction(
connection,
this.address,
ownerAddress,
account.publicKey,
this._programId,
),
);
openOrdersAddress = account.publicKey;
signers.push(account);
// refresh the cache of open order accounts on next fetch
this._openOrdersAccountsCache[ownerAddress.toBase58()].ts = 0;
} else if (openOrdersAccount) {
openOrdersAddress = openOrdersAccount.publicKey;
} else if (openOrdersAddressKey) {
openOrdersAddress = openOrdersAddressKey;
} else {
openOrdersAddress = openOrdersAccounts[0].address;
}
let wrappedSolAccount: Account | null = null;
if (payer.equals(ownerAddress)) {
if (
(side === 'buy' && this.quoteMintAddress.equals(WRAPPED_SOL_MINT)) ||
(side === 'sell' && this.baseMintAddress.equals(WRAPPED_SOL_MINT))
) {
wrappedSolAccount = new Account();
let lamports;
if (side === 'buy') {
lamports = Math.round(price * size * 1.01 * LAMPORTS_PER_SOL);
if (openOrdersAccounts.length > 0) {
lamports -= openOrdersAccounts[0].quoteTokenFree.toNumber();
}
} else {
lamports = Math.round(size * LAMPORTS_PER_SOL);
if (openOrdersAccounts.length > 0) {
lamports -= openOrdersAccounts[0].baseTokenFree.toNumber();
}
}
lamports = Math.max(lamports, 0) + 1e7;
transaction.add(
SystemProgram.createAccount({
fromPubkey: ownerAddress,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports,
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
);
transaction.add(
initializeAccount({
account: wrappedSolAccount.publicKey,
mint: WRAPPED_SOL_MINT,
owner: ownerAddress,
}),
);
signers.push(wrappedSolAccount);
} else {
throw new Error('Invalid payer account');
}
}
const placeOrderInstruction = this.makePlaceOrderInstruction(connection, {
owner,
payer: wrappedSolAccount?.publicKey ?? payer,
side,
price,
size,
orderType,
clientId,
openOrdersAddressKey: openOrdersAddress,
feeDiscountPubkey: useFeeDiscountPubkey,
selfTradeBehavior,
maxTs,
replaceIfExists,
});
transaction.add(placeOrderInstruction);
if (wrappedSolAccount) {
transaction.add(
closeAccount({
source: wrappedSolAccount.publicKey,
destination: ownerAddress,
owner: ownerAddress,
}),
);
}
return { transaction, signers, payer: owner };
}
makePlaceOrderInstruction<T extends PublicKey | Account>(
connection: Connection,
params: OrderParams<T>,
): TransactionInstruction {
const {
owner,
payer,
side,
price,
size,
orderType = 'limit',
clientId,
openOrdersAddressKey,
openOrdersAccount,
feeDiscountPubkey = null,
} = params;
// @ts-ignore
const ownerAddress: PublicKey = owner.publicKey ?? owner;
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');
}
if (this.usesRequestQueue) {
return DexInstructions.newOrder({
market: this.address,
requestQueue: this._decoded.requestQueue,
baseVault: this._decoded.baseVault,
quoteVault: this._decoded.quoteVault,
openOrders: openOrdersAccount
? openOrdersAccount.publicKey
: openOrdersAddressKey,
owner: ownerAddress,
payer,
side,
limitPrice: this.priceNumberToLots(price),
maxQuantity: this.baseSizeNumberToLots(size),
orderType,
clientId,
programId: this._programId,
// @ts-ignore
feeDiscountPubkey: this.supportsSrmFeeDiscounts
? feeDiscountPubkey
: null,
});
} else {
return this.makeNewOrderV3Instruction(params);
}
}
makeNewOrderV3Instruction<T extends PublicKey | Account>(
params: OrderParams<T>,
): TransactionInstruction {
const {
owner,
payer,
side,
price,
size,
orderType = 'limit',
clientId,
openOrdersAddressKey,
openOrdersAccount,
feeDiscountPubkey = null,
selfTradeBehavior = 'decrementTake',
programId,
maxTs,
replaceIfExists,
} = params;
// @ts-ignore
const ownerAddress: PublicKey = owner.publicKey ?? owner;
return DexInstructions.newOrderV3({
market: this.address,
bids: this._decoded.bids,
asks: this._decoded.asks,
requestQueue: this._decoded.requestQueue,
eventQueue: this._decoded.eventQueue,
baseVault: this._decoded.baseVault,
quoteVault: this._decoded.quoteVault,
openOrders: openOrdersAccount
? openOrdersAccount.publicKey
: openOrdersAddressKey,
owner: ownerAddress,
payer,
side,
limitPrice: this.priceNumberToLots(price),
maxBaseQuantity: this.baseSizeNumberToLots(size),
maxQuoteQuantity: new BN(this._decoded.quoteLotSize.toNumber()).mul(
this.baseSizeNumberToLots(size).mul(this.priceNumberToLots(price)),
),
orderType,
clientId,
programId: programId ?? this._programId,
selfTradeBehavior,
// @ts-ignore
feeDiscountPubkey: this.supportsSrmFeeDiscounts
? feeDiscountPubkey
: null,
// @ts-ignore
maxTs,
replaceIfExists,
});
}
async makeSendTakeTransaction<T extends PublicKey | Account>(
connection: Connection,
{
owner,
baseWallet,
quoteWallet,
side,
price,
maxBaseSize,
maxQuoteSize,
minBaseSize,
minQuoteSize,
limit = 65535,
programId = undefined,
feeDiscountPubkey = undefined,
}: SendTakeParams<T>,
feeDiscountPubkeyCacheDurationMs = 0
) {
// @ts-ignore
const ownerAddress: PublicKey = owner.publicKey ?? owner;
const transaction = new Transaction();
const signers: Account[] = [];
// @ts-ignore
const vaultSigner = await PublicKey.createProgramAddress(
[
this.address.toBuffer(),
this._decoded.vaultSignerNonce.toArrayLike(Buffer, 'le', 8),
],
this._programId,
);
// Fetch an SRM fee discount key if the market supports discounts and it is not supplied
let useFeeDiscountPubkey: PublicKey | null;
if (feeDiscountPubkey) {
useFeeDiscountPubkey = feeDiscountPubkey;
} else if (
feeDiscountPubkey === undefined &&
this.supportsSrmFeeDiscounts
) {
useFeeDiscountPubkey = (
await this.findBestFeeDiscountKey(
connection,
ownerAddress,
feeDiscountPubkeyCacheDurationMs,
)
).pubkey;
} else {
useFeeDiscountPubkey = null;
}
const sendTakeInstruction = this.makeSendTakeInstruction({
owner,
baseWallet,
quoteWallet,
vaultSigner,
side,
price,
maxBaseSize,
maxQuoteSize,
minBaseSize,
minQuoteSize,
limit,
programId,
feeDiscountPubkey: useFeeDiscountPubkey,
});
transaction.add(sendTakeInstruction);
return { transaction, signers, payer: owner };
}
makeSendTakeInstruction<T extends PublicKey | Account>(
params: SendTakeParams<T>,
): TransactionInstruction {
const {
owner,
baseWallet,
quoteWallet,
vaultSigner,
side,
price,
maxBaseSize,
maxQuoteSize,
minBaseSize,
minQuoteSize,
limit = 65535,
programId,
feeDiscountPubkey = null,
} = params;
// @ts-ignore
const ownerAddress: PublicKey = owner.publicKey ?? owner;
if (this.baseSizeNumberToLots(maxBaseSize).lte(new BN(0))) {
throw new Error('size too small');
}
if (this.quoteSizeNumberToSplSize(maxQuoteSize).lte(new BN(0))) {
throw new Error('size too small');
}
if (this.priceNumberToLots(price).lte(new BN(0))) {
throw new Error('invalid price');
}
return DexInstructions.sendTake({
market: this.address,
requestQueue: this._decoded.requestQueue,
eventQueue: this._decoded.eventQueue,
bids: this._decoded.bids,
asks: this._decoded.asks,
baseWallet,
quoteWallet,
owner: ownerAddress,
baseVault: this._decoded.baseVault,
quoteVault: this._decoded.quoteVault,
vaultSigner,
side,
limitPrice: this.priceNumberToLots(price),
maxBaseQuantity: this.baseSizeNumberToLots(maxBaseSize),
maxQuoteQuantity: this.quoteSizeNumberToSplSize(maxQuoteSize),
minBaseQuantity: this.baseSizeNumberToLots(minBaseSize),
minQuoteQuantity: this.quoteSizeNumberToSplSize(minQuoteSize),
limit,
programId: programId ? programId : this._programId,
// @ts-ignore
feeDiscountPubkey: this.supportsSrmFeeDiscounts
? feeDiscountPubkey
: null,
});
}
makeReplaceOrdersByClientIdsInstruction<T extends PublicKey | Account>(
accounts: OrderParamsAccounts<T>, orders: OrderParamsBase<T>[],
): TransactionInstruction {
// @ts-ignore
const ownerAddress: PublicKey = accounts.owner.publicKey ?? accounts.owner;
return DexInstructions.replaceOrdersByClientIds({
market: this.address,
bids: this._decoded.bids,
asks: this._decoded.asks,
requestQueue: this._decoded.requestQueue,
eventQueue: this._decoded.eventQueue,
baseVault: this._decoded.baseVault,
quoteVault: this._decoded.quoteVault,
openOrders: accounts.openOrdersAccount
? accounts.openOrdersAccount.publicKey
: accounts.openOrdersAddressKey,
owner: ownerAddress,
payer: accounts.payer,
programId: accounts.programId ?? this._programId,
// @ts-ignore
feeDiscountPubkey: this.supportsSrmFeeDiscounts
? accounts.feeDiscountPubkey
: null,
orders: orders.map(order => ({
side: order.side,
limitPrice: this.priceNumberToLots(order.price),
maxBaseQuantity: this.baseSizeNumberToLots(order.size),
maxQuoteQuantity: new BN(this._decoded.quoteLotSize.toNumber()).mul(
this.baseSizeNumberToLots(order.size).mul(this.priceNumberToLots(order.price)),
),
orderType: order.orderType,
clientId: order.clientId,
programId: accounts.programId ?? this._programId,
selfTradeBehavior: order.selfTradeBehavior,
// @ts-ignore
maxTs: order.maxTs,
}))
});
}
private async _sendTransaction(
connection: Connection,
transaction: Transaction,
signers: Array<Account>,
): Promise<TransactionSignature> {
const signature = await connection.sendTransaction(transaction, signers, {
skipPreflight: this._skipPreflight,
});
const { value } = await connection.confirmTransaction(
signature,
this._commitment,
);
if (value?.err) {
throw new Error(JSON.stringify(value.err));
}
return signature;
}
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 cancelOrdersByClientIds(
connection: Connection,
owner: Account,
openOrders: PublicKey,
clientIds: BN[],
) {
const transaction = await this.makeCancelOrdersByClientIdsTransaction(
connection,
owner.publicKey,
openOrders,
clientIds,
);
return await this._sendTransaction(connection, transaction, [owner]);
}
async makeCancelOrderByClientIdTransaction(
connection: Connection,
owner: PublicKey,
openOrders: PublicKey,
clientId: BN,
) {
const transaction = new Transaction();
if (this.usesRequestQueue) {
transaction.add(
DexInstructions.cancelOrderByClientId({
market: this.address,
owner,
openOrders,
requestQueue: this._decoded.requestQueue,
clientId,
programId: this._programId,
}),
);
} else {
transaction.add(
DexInstructions.cancelOrderByClientIdV2({
market: this.address,
openOrders,
owner,
bids: this._decoded.bids,
asks: this._decoded.asks,
eventQueue: this._decoded.eventQueue,
clientId,
programId: this._programId,
}),
);
}
return transaction;
}
async makeCancelOrdersByClientIdsTransaction(
connection: Connection,
owner: PublicKey,
openOrders: PublicKey,
clientIds: BN[],
) {
const transaction = new Transaction();
transaction.add(
DexInstructions.cancelOrdersByClientIds({
market: this.address,
openOrders,
owner,
bids: this._decoded.bids,
asks: this._decoded.asks,
eventQueue: this._decoded.eventQueue,
clientIds,
programId: this._programId,
}),
);
return transaction;
}
async cancelOrder(connection: Connection, owner: Account, order: Order) {
const transaction = await this.makeCancelOrderTransaction(
connection,
owner.publicKey,
order,
);
return await this._sendTransaction(connection, transaction, [owner]);
}
async makeCancelOrderTransaction(
connection: Connection,
owner: PublicKey,
order: Order,
) {
const transaction = new Transaction();
transaction.add(this.makeCancelOrderInstruction(connection, owner, order));
return transaction;
}
makeCancelOrderInstruction(
connection: Connection,
owner: PublicKey,
order: Order,
) {
if (this.usesRequestQueue) {
return DexInstructions.cancelOrder({
market: this.address,
owner,
openOrders: order.openOrdersAddress,
requestQueue: this._decoded.requestQueue,
side: order.side,
orderId: order.orderId,
openOrdersSlot: order.openOrdersSlot,
programId: this._programId,
});
} else {
return DexInstructions.cancelOrderV2({
market: this.address,
owner,
openOrders: order.openOrdersAddress,
bids: this._decoded.bids,
asks: this._decoded.asks,
eventQueue: this._decoded.eventQueue,
side: order.side,
orderId: order.orderId,
openOrdersSlot: order.openOrdersSlot,
programId: this._programId,
});
}
}
public makeConsumeEventsInstruction(
openOrdersAccounts: Array<PublicKey>,
limit: number,
): TransactionInstruction {
return DexInstructions.consumeEvents({
market: this.address,
eventQueue: this._decoded.eventQueue,
coinFee: this._decoded.eventQueue,
pcFee: this._decoded.eventQueue,
openOrdersAccounts,
limit,
programId: this._programId,
});
}
public makeConsumeEventsPermissionedInstruction(
openOrdersAccounts: Array<PublicKey>,
limit: number,
): TransactionInstruction {
return DexInstructions.consumeEventsPermissioned({
market: this.address,
eventQueue: this._decoded.eventQueue,
crankAuthority: this._decoded.consumeEventsAuthority,
openOrdersAccounts,
limit,
programId: this._programId,
});
}
async settleFunds(
connection: Connection,
owner: Account,
openOrders: OpenOrders,
baseWallet: PublicKey,
quoteWallet: PublicKey,
referrerQuoteWallet: PublicKey | null = null,
) {
if (!openOrders.owner.equals(owner.publicKey)) {
throw new Error('Invalid open orders account');
}
if (referrerQuoteWallet && !this.supportsReferralFees) {
throw new Error('This program ID does not support referrerQuoteWallet');
}
const { transaction, signers } = await this.makeSettleFundsTransaction(
connection,
openOrders,
baseWallet,
quoteWallet,
referrerQuoteWallet,
);
return await this._sendTransaction(connection, transaction, [
owner,
...signers,
]);
}
async makeSettleFundsTransaction(
connection: Connection,
openOrders: OpenOrders,
baseWallet: PublicKey,
quoteWallet: PublicKey,
referrerQuoteWallet: PublicKey | null = null,
) {
// @ts-ignore
const vaultSigner = await PublicKey.createProgramAddress(
[
this.address.toBuffer(),
this._decoded.vaultSignerNonce.toArrayLike(Buffer, 'le', 8),
],
this._programId,
);
const transaction = new Transaction();
const signers: Account[] = [];
let wrappedSolAccount: Account | null = null;
if (
(this.baseMintAddress.equals(WRAPPED_SOL_MINT) &&
baseWallet.equals(openOrders.owner)) ||
(this.quoteMintAddress.equals(WRAPPED_SOL_MINT) &&
quoteWallet.equals(openOrders.owner))
) {
wrappedSolAccount = new Account();
transaction.add(
SystemProgram.createAccount({
fromPubkey: openOrders.owner,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
);
transaction.add(
initializeAccount({
account: wrappedSolAccount.publicKey,
mint: WRAPPED_SOL_MINT,
owner: openOrders.owner,
}),
);
signers.push(wrappedSolAccount);
}
transaction.add(
DexInstructions.settleFunds({
market: this.address,
openOrders: openOrders.address,
owner: openOrders.owner,
baseVault: this._decoded.baseVault,
quoteVault: this._decoded.quoteVault,
baseWallet:
baseWallet.equals(openOrders.owner) && wrappedSolAccount
? wrappedSolAccount.publicKey
: baseWallet,
quoteWallet:
quoteWallet.equals(openOrders.owner) && wrappedSolAccount
? wrappedSolAccount.publicKey
: quoteWallet,
vaultSigner,
programId: this._programId,
// @ts-ignore
referrerQuoteWallet,
}),
);
if (wrappedSolAccount) {
transaction.add(
closeAccount({
source: wrappedSolAccount.publicKey,
destination: openOrders.owner,
owner: openOrders.owner,
}),
);
}
return { transaction, signers, payer: openOrders.owner };
}
async matchOrders(connection: Connection, feePayer: Account, limit: number) {
const tx = this.makeMatchOrdersTransaction(limit);
return await this._sendTransaction(connection, tx, [feePayer]);
}
makeMatchOrdersTransaction(limit: number): Transaction {
const tx = new Transaction();
tx.add(
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,
programId: this._programId,
}),
);
return tx;
}
async loadRequestQueue(connection: Connection) {
const { data } = throwIfNull(
await connection.getAccountInfo(this._decoded.requestQueue),
);
return decodeRequestQueue(data);
}
async loadEventQueue(connection: Connection) {
const { data } = throwIfNull(
await connection.getAccountInfo(this._decoded.eventQueue),
);
return decodeEventQueue(data);
}
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
.filter(
(event: any) => event.eventFlags.fill && event.nativeQuantityPaid.gtn(0),
)
.map(this.parseFillEvent.bind(this));
}
parseFillEvent(event: any) {
let size, price, side, priceBeforeFees;
if (event.eventFlags.bid) {
side = 'buy';
priceBeforeFees = event.eventFlags.maker
? event.nativeQuantityPaid.add(event.nativeFeeOrRebate)
: event.nativeQuantityPaid.sub(event.nativeFeeOrRebate);
price = divideBnToNumber(
priceBeforeFees.mul(this._baseSplTokenMultiplier),
this._quoteSplTokenMultiplier.mul(event.nativeQuantityReleased),
);
size = divideBnToNumber(
event.nativeQuantityReleased,
this._baseSplTokenMultiplier,
);
} else {
side = 'sell';
priceBeforeFees = event.eventFlags.maker
? event.nativeQuantityReleased.sub(event.nativeFeeOrRebate)
: event.nativeQuantityReleased.add(event.nativeFeeOrRebate);
price = divideBnToNumber(
priceBeforeFees.mul(this._baseSplTokenMultiplier),
this._quoteSplTokenMultiplier.mul(event.nativeQuantityPaid),
);
size = divideBnToNumber(
event.nativeQuantityPaid,
this._baseSplTokenMultiplier,
);
}
return {
...event,
side,
price,
feeCost:
this.quoteSplSizeToNumber(event.nativeFeeOrRebate) *
(event.eventFlags.maker ? -1 : 1),
size,
};
}
private get _baseSplTokenMultiplier() {
return new BN(10).pow(new BN(this._baseSplTokenDecimals));
}
private get _quoteSplTokenMultiplier() {
return new BN(10).pow(new BN(this._quoteSplTokenDecimals));
}
priceLotsToNumber(price: BN) {
return divideBnToNumber(
price.mul(this._decoded.quoteLotSize).mul(this._baseSplTokenMultiplier),
this._decoded.baseLotSize.mul(this._quoteSplTokenMultiplier),
);
}
priceNumberToLots(price: number): BN {
return new BN(
Math.round(
(price *
Math.pow(10, this._quoteSplTokenDecimals) *
this._decoded.baseLotSize.toNumber()) /
(Math.pow(10, this._baseSplTokenDecimals) *
this._decoded.quoteLotSize.toNumber()),
),
);
}
baseSplSizeToNumber(size: BN) {
return divideBnToNumber(size, this._baseSplTokenMultiplier);
}
quoteSplSizeToNumber(size: BN) {
return divideBnToNumber(size, this._quoteSplTokenMultiplier);
}
baseSizeNumberToSplSize(size: number) {
return new BN(Math.round(size * Math.pow(10, this._baseSplTokenDecimals)),);
}
quoteSizeNumberToSplSize(size: number) {
return new BN(Math.round(size * Math.pow(10, this._quoteSplTokenDecimals)),);
}
baseSizeLotsToNumber(size: BN) {
return divideBnToNumber(
size.mul(this._decoded.baseLotSize),
this._baseSplTokenMultiplier,
);
}
baseSizeNumberToLots(size: number): BN {
const native = new BN(
Math.round(size * Math.pow(10, this._baseSplTokenDecimals)),
);
// rounds down to the nearest lot size
return native.div(this._decoded.baseLotSize);
}
quoteSizeLotsToNumber(size: BN) {
return divideBnToNumber(
size.mul(this._decoded.quoteLotSize),
this._quoteSplTokenMultiplier,
);
}
quoteSizeNumberToLots(size: number): BN {
const native = new BN(
Math.round(size * Math.pow(10, this._quoteSplTokenDecimals)),
);
// roudns down to the nearest lot size
return native.div(this._decoded.quoteLotSize);
}
get minOrderSize() {
return this.baseSizeLotsToNumber(new BN(1));
}
get tickSize() {
return this.priceLotsToNumber(new BN(1));
}
}
export interface MarketOptions {
skipPreflight?: boolean;
commitment?: Commitment;
}
export interface OrderParamsBase<T = Account> {
side: 'buy' | 'sell';
price: number;
size: number;
orderType?: 'limit' | 'ioc' | 'postOnly';
clientId?: BN;
selfTradeBehavior?:
| 'decrementTake'
| 'cancelProvide'
| 'abortTransaction'
| undefined;
maxTs?: number | null;
}
export interface OrderParamsAccounts<T = Account> {
owner: T;
payer: PublicKey;
openOrdersAddressKey?: PublicKey;
openOrdersAccount?: Account;
feeDiscountPubkey?: PublicKey | null;
programId?: PublicKey;
}
export interface OrderParams<T = Account> extends OrderParamsBase<T>, OrderParamsAccounts<T> {
replaceIfExists?: boolean;
}
export interface SendTakeParamsBase<T = Account> {
side: 'buy' | 'sell';
price: number;
maxBaseSize: number;
maxQuoteSize: number;
minBaseSize: number;
minQuoteSize: number;
limit?: number;
}
export interface SendTakeParamsAccounts<T = Account> {
owner: T;
baseWallet: PublicKey;
quoteWallet: PublicKey;
vaultSigner?: PublicKey;
feeDiscountPubkey?: PublicKey | null;
programId?: PublicKey;
}
export interface SendTakeParams<T = Account> extends SendTakeParamsBase<T>, SendTakeParamsAccounts<T> { }
export const _OPEN_ORDERS_LAYOUT_V1 = struct([
blob(5),
accountFlagsLayout('accountFlags'),
publicKeyLayout('market'),
publicKeyLayout('owner'),
// These are in spl-token (i.e. not lot) units
u64('baseTokenFree'),
u64('baseTokenTotal'),
u64('quoteTokenFree'),
u64('quoteTokenTotal'),
u128('freeSlotBits'),
u128('isBidBits'),
seq(u128(), 128, 'orders'),
seq(u64(), 128, 'clientIds'),
blob(7),
]);
export const _OPEN_ORDERS_LAYOUT_V2 = struct([
blob(5),
accountFlagsLayout('accountFlags'),
publicKeyLayout('market'),
publicKeyLayout('owner'),
// These are in spl-token (i.e. not lot) units
u64('baseTokenFree'),
u64('baseTokenTotal'),
u64('quoteTokenFree'),
u64('quoteTokenTotal'),
u128('freeSlotBits'),
u128('isBidBits'),
seq(u128(), 128, 'orders'),
seq(u64(), 128, 'clientIds'),
u64('referrerRebatesAccrued'),
blob(7),
]);
export class OpenOrders {
private _programId: PublicKey;
address: PublicKey;
market!: PublicKey;
owner!: PublicKey;
baseTokenFree!: BN;
baseTokenTotal!: BN;
quoteTokenFree!: BN;
quoteTokenTotal!: BN;
freeSlotBits!: BN;
isBidBits!: BN;
orders!: BN[];
clientIds!: BN[];
constructor(address: PublicKey, decoded: any, programId: PublicKey) {
this.address = address;
this._programId = programId;
Object.assign(this, decoded);
}
static getLayout(programId: PublicKey) {
if (getLayoutVersion(programId) === 1) {
return _OPEN_ORDERS_LAYOUT_V1;
}
return _OPEN_ORDERS_LAYOUT_V2;
}
static async findForOwner(
connection: Connection,
ownerAddress: PublicKey,
programId: PublicKey,
) {
const filters = [
{
memcmp: {
offset: this.getLayout(programId).offsetOf('owner'),
bytes: ownerAddress.toBase58(),
},
},
{
dataSize: this.getLayout(programId).span,
},
];
const accounts = await getFilteredProgramAccounts(
connection,
programId,
filters,
);
return accounts.map(({ publicKey, accountInfo }) =>
OpenOrders.fromAccountInfo(publicKey, accountInfo, programId),
);
}
static async findForMarketAndOwner(
connection: Connection,
marketAddress: PublicKey,
ownerAddress: PublicKey,
programId: PublicKey,
) {
const filters = [
{
memcmp: {
offset: this.getLayout(programId).offsetOf('market'),
bytes: marketAddress.toBase58(),
},
},
{
memcmp: {
offset: this.getLayout(programId).offsetOf('owner'),
bytes: ownerAddress.toBase58(),
},
},
{
dataSize: this.getLayout(programId).span,
},
];
const accounts = await getFilteredProgramAccounts(
connection,
programId,
filters,
);
return accounts.map(({ publicKey, accountInfo }) =>
OpenOrders.fromAccountInfo(publicKey, accountInfo, programId),
);
}
static async load(
connection: Connection,
address: PublicKey,
programId: PublicKey,
) {
const accountInfo = await connection.getAccountInfo(address);
if (accountInfo === null) {
throw new Error('Open orders account not found');
}
return OpenOrders.fromAccountInfo(address, accountInfo, programId);
}
static fromAccountInfo(
address: PublicKey,
accountInfo: AccountInfo<Buffer>,
programId: PublicKey,
) {
const { owner, data } = accountInfo;
if (!owner.equals(programId)) {
throw new Error('Address not owned by program');
}
const decoded = this.getLayout(programId).decode(data);
if (!decoded.accountFlags.initialized || !decoded.accountFlags.openOrders) {
throw new Error('Invalid open orders account');
}
return new OpenOrders(address, decoded, programId);
}
static async makeCreateAccountTransaction(
connection: Connection,
marketAddress: PublicKey,
ownerAddress: PublicKey,
newAccountAddress: PublicKey,
programId: PublicKey,
) {
return SystemProgram.createAccount({
fromPubkey: ownerAddress,
newAccountPubkey: newAccountAddress,
lamports: await connection.getMinimumBalanceForRentExemption(
this.getLayout(programId).span,
),
space: this.getLayout(programId).span,
programId,
});
}
get publicKey() {
return this.address;
}
}
export const ORDERBOOK_LAYOUT = struct([
blob(5),
accountFlagsLayout('accountFlags'),
SLAB_LAYOUT.replicate('slab'),
blob(7),
]);
export class Orderbook {
market: Market;
isBids: boolean;
slab: Slab;
constructor(market: Market, accountFlags: any, slab: Slab) {
if (!accountFlags.initialized || !(accountFlags.bids ^ accountFlags.asks)) {
throw new Error('Invalid orderbook');
}
this.market = market;
this.isBids = accountFlags.bids;
this.slab = slab;
}
static get LAYOUT() {
return ORDERBOOK_LAYOUT;
}
static decode(market: Market, buffer: Buffer) {
const { accountFlags, slab } = ORDERBOOK_LAYOUT.decode(buffer);
return new Orderbook(market, accountFlags, slab);
}
getL2(depth: number): [number, number, BN, BN][] {
const descending = this.isBids;
const levels: [BN, BN][] = []; // (price, size)
for (cons