@thespidercode/openbook-swap
Version:
Ready-to-use swap tool using Openbook DEX
1,151 lines • 52.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeInstruction = exports.getMintDecimals = exports.Orderbook = exports.ORDERBOOK_LAYOUT = exports.OpenOrders = exports._OPEN_ORDERS_LAYOUT_V2 = exports._OPEN_ORDERS_LAYOUT_V1 = exports.Market = exports.MARKET_STATE_LAYOUT_V3 = exports.MARKET_STATE_LAYOUT_V2 = exports._MARKET_STAT_LAYOUT_V1 = void 0;
const web3_js_1 = require("@solana/web3.js");
const bn_js_1 = __importDefault(require("bn.js"));
const buffer_1 = require("buffer");
const buffer_layout_1 = require("buffer-layout");
const fees_1 = require("./fees");
const instructions_1 = require("./instructions");
const layout_1 = require("./layout");
const queue_1 = require("./queue");
const slab_1 = require("./slab");
const token_instructions_1 = require("./token-instructions");
const tokens_and_markets_1 = require("./tokens_and_markets");
exports._MARKET_STAT_LAYOUT_V1 = (0, buffer_layout_1.struct)([
(0, buffer_layout_1.blob)(5),
(0, layout_1.accountFlagsLayout)('accountFlags'),
(0, layout_1.publicKeyLayout)('ownAddress'),
(0, layout_1.u64)('vaultSignerNonce'),
(0, layout_1.publicKeyLayout)('baseMint'),
(0, layout_1.publicKeyLayout)('quoteMint'),
(0, layout_1.publicKeyLayout)('baseVault'),
(0, layout_1.u64)('baseDepositsTotal'),
(0, layout_1.u64)('baseFeesAccrued'),
(0, layout_1.publicKeyLayout)('quoteVault'),
(0, layout_1.u64)('quoteDepositsTotal'),
(0, layout_1.u64)('quoteFeesAccrued'),
(0, layout_1.u64)('quoteDustThreshold'),
(0, layout_1.publicKeyLayout)('requestQueue'),
(0, layout_1.publicKeyLayout)('eventQueue'),
(0, layout_1.publicKeyLayout)('bids'),
(0, layout_1.publicKeyLayout)('asks'),
(0, layout_1.u64)('baseLotSize'),
(0, layout_1.u64)('quoteLotSize'),
(0, layout_1.u64)('feeRateBps'),
(0, buffer_layout_1.blob)(7),
]);
exports.MARKET_STATE_LAYOUT_V2 = (0, buffer_layout_1.struct)([
(0, buffer_layout_1.blob)(5),
(0, layout_1.accountFlagsLayout)('accountFlags'),
(0, layout_1.publicKeyLayout)('ownAddress'),
(0, layout_1.u64)('vaultSignerNonce'),
(0, layout_1.publicKeyLayout)('baseMint'),
(0, layout_1.publicKeyLayout)('quoteMint'),
(0, layout_1.publicKeyLayout)('baseVault'),
(0, layout_1.u64)('baseDepositsTotal'),
(0, layout_1.u64)('baseFeesAccrued'),
(0, layout_1.publicKeyLayout)('quoteVault'),
(0, layout_1.u64)('quoteDepositsTotal'),
(0, layout_1.u64)('quoteFeesAccrued'),
(0, layout_1.u64)('quoteDustThreshold'),
(0, layout_1.publicKeyLayout)('requestQueue'),
(0, layout_1.publicKeyLayout)('eventQueue'),
(0, layout_1.publicKeyLayout)('bids'),
(0, layout_1.publicKeyLayout)('asks'),
(0, layout_1.u64)('baseLotSize'),
(0, layout_1.u64)('quoteLotSize'),
(0, layout_1.u64)('feeRateBps'),
(0, layout_1.u64)('referrerRebatesAccrued'),
(0, buffer_layout_1.blob)(7),
]);
exports.MARKET_STATE_LAYOUT_V3 = (0, buffer_layout_1.struct)([
(0, buffer_layout_1.blob)(5),
(0, layout_1.accountFlagsLayout)('accountFlags'),
(0, layout_1.publicKeyLayout)('ownAddress'),
(0, layout_1.u64)('vaultSignerNonce'),
(0, layout_1.publicKeyLayout)('baseMint'),
(0, layout_1.publicKeyLayout)('quoteMint'),
(0, layout_1.publicKeyLayout)('baseVault'),
(0, layout_1.u64)('baseDepositsTotal'),
(0, layout_1.u64)('baseFeesAccrued'),
(0, layout_1.publicKeyLayout)('quoteVault'),
(0, layout_1.u64)('quoteDepositsTotal'),
(0, layout_1.u64)('quoteFeesAccrued'),
(0, layout_1.u64)('quoteDustThreshold'),
(0, layout_1.publicKeyLayout)('requestQueue'),
(0, layout_1.publicKeyLayout)('eventQueue'),
(0, layout_1.publicKeyLayout)('bids'),
(0, layout_1.publicKeyLayout)('asks'),
(0, layout_1.u64)('baseLotSize'),
(0, layout_1.u64)('quoteLotSize'),
(0, layout_1.u64)('feeRateBps'),
(0, layout_1.u64)('referrerRebatesAccrued'),
(0, layout_1.publicKeyLayout)('authority'),
(0, layout_1.publicKeyLayout)('pruneAuthority'),
(0, layout_1.publicKeyLayout)('consumeEventsAuthority'),
(0, buffer_layout_1.blob)(992),
(0, buffer_layout_1.blob)(7),
]);
class Market {
_decoded;
_baseSplTokenDecimals;
_quoteSplTokenDecimals;
_skipPreflight;
_commitment;
_programId;
_openOrdersAccountsCache;
_layoutOverride;
_feeDiscountKeysCache;
constructor(decoded, baseMintDecimals, quoteMintDecimals, options = {}, programId, layoutOverride) {
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) {
if ((0, tokens_and_markets_1.getLayoutVersion)(programId) === 1) {
return exports._MARKET_STAT_LAYOUT_V1;
}
return exports.MARKET_STATE_LAYOUT_V2;
}
static async findAccountsByMints(connection, baseMintAddress, quoteMintAddress, programId) {
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, address, options = {}, programId, layoutOverride) {
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() {
return this._programId;
}
get address() {
return this._decoded.ownAddress;
}
get publicKey() {
return this.address;
}
get baseMintAddress() {
return this._decoded.baseMint;
}
get quoteMintAddress() {
return this._decoded.quoteMint;
}
get bidsAddress() {
return this._decoded.bids;
}
get asksAddress() {
return this._decoded.asks;
}
get decoded() {
return this._decoded;
}
async loadBids(connection) {
const { data } = throwIfNull(await connection.getAccountInfo(this._decoded.bids));
return Orderbook.decode(this, data);
}
async loadAsks(connection) {
const { data } = throwIfNull(await connection.getAccountInfo(this._decoded.asks));
return Orderbook.decode(this, data);
}
async loadOrdersForOwner(connection, ownerAddress, cacheDurationMs = 0) {
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, asks, openOrdersAccounts) {
return [...bids, ...asks].filter((order) => openOrdersAccounts.some((openOrders) => order.openOrdersAddress.equals(openOrders.address)));
}
async findBaseTokenAccountsForOwner(connection, ownerAddress, includeUnwrappedSol = false) {
if (this.baseMintAddress.equals(token_instructions_1.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, ownerAddress, mintAddress) {
const response = await connection.getTokenAccountsByOwner(ownerAddress, {
mint: mintAddress,
});
// Convert the response to a mutable array
const accounts = [...response.value];
return accounts;
}
async findQuoteTokenAccountsForOwner(connection, ownerAddress, includeUnwrappedSol = false) {
if (this.quoteMintAddress.equals(token_instructions_1.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, ownerAddress, cacheDurationMs = 0) {
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, accounts, orders, cacheDurationMs = 0) {
if (!accounts.openOrdersAccount && !accounts.openOrdersAddressKey) {
const ownerAddress = accounts.owner.publicKey ?? accounts.owner;
const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner(connection, ownerAddress, cacheDurationMs);
accounts.openOrdersAddressKey = openOrdersAccounts[0].address;
}
const transaction = new web3_js_1.Transaction();
transaction.add(this.makeReplaceOrdersByClientIdsInstruction(accounts, orders));
return await this._sendTransaction(connection, transaction, [
accounts.owner,
]);
}
async placeOrder(connection, { owner, payer, side, price, size, orderType = 'limit', clientId, openOrdersAddressKey, openOrdersAccount, feeDiscountPubkey, maxTs, replaceIfExists = false, }) {
const { transaction, signers } = await this.makePlaceOrderTransaction(connection, {
owner,
payer,
side,
price,
size,
orderType,
clientId,
openOrdersAddressKey,
openOrdersAccount,
feeDiscountPubkey,
maxTs,
replaceIfExists,
});
return await this._sendTransaction(connection, transaction, [
owner,
...signers,
]);
}
async sendTake(connection, { owner, baseWallet, quoteWallet, side, price, maxBaseSize, maxQuoteSize, minBaseSize, minQuoteSize, limit = 65535, programId = undefined, feeDiscountPubkey = undefined, }) {
const { transaction, signers } = await this.makeSendTakeTransaction(connection, {
owner,
baseWallet,
quoteWallet,
side,
price,
maxBaseSize,
maxQuoteSize,
minBaseSize,
minQuoteSize,
limit,
programId,
feeDiscountPubkey,
});
return await this._sendTransaction(connection, transaction, [
owner,
...signers
]);
}
getSplTokenBalanceFromAccountInfo(accountInfo, decimals) {
return divideBnToNumber(new bn_js_1.default(accountInfo.data.slice(64, 72), 10, 'le'), new bn_js_1.default(10).pow(new bn_js_1.default(decimals)));
}
get supportsSrmFeeDiscounts() {
return (0, fees_1.supportsSrmFeeDiscounts)(this._programId);
}
get supportsReferralFees() {
return (0, tokens_and_markets_1.getLayoutVersion)(this._programId) > 1;
}
get usesRequestQueue() {
return (0, tokens_and_markets_1.getLayoutVersion)(this._programId) <= 2;
}
async findFeeDiscountKeys(connection, ownerAddress, cacheDurationMs = 0) {
let sortedAccounts = [];
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, token_instructions_1.MSRM_MINT)).map(({ pubkey, account }) => {
const balance = this.getSplTokenBalanceFromAccountInfo(account, token_instructions_1.MSRM_DECIMALS);
return {
pubkey,
mint: token_instructions_1.MSRM_MINT,
balance,
feeTier: (0, fees_1.getFeeTier)(balance, 0),
};
});
const srmAccounts = (await this.getTokenAccountsByOwnerForMint(connection, ownerAddress, token_instructions_1.SRM_MINT)).map(({ pubkey, account }) => {
const balance = this.getSplTokenBalanceFromAccountInfo(account, token_instructions_1.SRM_DECIMALS);
return {
pubkey,
mint: token_instructions_1.SRM_MINT,
balance,
feeTier: (0, fees_1.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, ownerAddress, cacheDurationMs = 30000) {
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(connection, { owner, payer, side, price, size, orderType = 'limit', clientId, openOrdersAddressKey, openOrdersAccount, feeDiscountPubkey = undefined, selfTradeBehavior = 'decrementTake', maxTs, replaceIfExists = false, }, cacheDurationMs = 0, feeDiscountPubkeyCacheDurationMs = 0) {
// @ts-ignore
const ownerAddress = owner.publicKey ?? owner;
const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner(connection, ownerAddress, cacheDurationMs);
const transaction = new web3_js_1.Transaction();
const signers = [];
// Fetch an SRM fee discount key if the market supports discounts and it is not supplied
let useFeeDiscountPubkey;
if (feeDiscountPubkey) {
useFeeDiscountPubkey = feeDiscountPubkey;
}
else if (feeDiscountPubkey === undefined &&
this.supportsSrmFeeDiscounts) {
useFeeDiscountPubkey = (await this.findBestFeeDiscountKey(connection, ownerAddress, feeDiscountPubkeyCacheDurationMs)).pubkey;
}
else {
useFeeDiscountPubkey = null;
}
let openOrdersAddress;
if (openOrdersAccounts.length === 0) {
let account;
if (openOrdersAccount) {
account = openOrdersAccount;
}
else {
account = new web3_js_1.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 = null;
if (payer.equals(ownerAddress)) {
if ((side === 'buy' && this.quoteMintAddress.equals(token_instructions_1.WRAPPED_SOL_MINT)) ||
(side === 'sell' && this.baseMintAddress.equals(token_instructions_1.WRAPPED_SOL_MINT))) {
wrappedSolAccount = new web3_js_1.Account();
let lamports;
if (side === 'buy') {
lamports = Math.round(price * size * 1.01 * web3_js_1.LAMPORTS_PER_SOL);
if (openOrdersAccounts.length > 0) {
lamports -= openOrdersAccounts[0].quoteTokenFree.toNumber();
}
}
else {
lamports = Math.round(size * web3_js_1.LAMPORTS_PER_SOL);
if (openOrdersAccounts.length > 0) {
lamports -= openOrdersAccounts[0].baseTokenFree.toNumber();
}
}
lamports = Math.max(lamports, 0) + 1e7;
transaction.add(web3_js_1.SystemProgram.createAccount({
fromPubkey: ownerAddress,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports,
space: 165,
programId: token_instructions_1.TOKEN_PROGRAM_ID,
}));
transaction.add((0, token_instructions_1.initializeAccount)({
account: wrappedSolAccount.publicKey,
mint: token_instructions_1.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((0, token_instructions_1.closeAccount)({
source: wrappedSolAccount.publicKey,
destination: ownerAddress,
owner: ownerAddress,
}));
}
return { transaction, signers, payer: owner };
}
makePlaceOrderInstruction(connection, params) {
const { owner, payer, side, price, size, orderType = 'limit', clientId, openOrdersAddressKey, openOrdersAccount, feeDiscountPubkey = null, } = params;
// @ts-ignore
const ownerAddress = owner.publicKey ?? owner;
if (this.baseSizeNumberToLots(size).lte(new bn_js_1.default(0))) {
throw new Error('size too small');
}
if (this.priceNumberToLots(price).lte(new bn_js_1.default(0))) {
throw new Error('invalid price');
}
if (this.usesRequestQueue) {
return instructions_1.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(params) {
const { owner, payer, side, price, size, orderType = 'limit', clientId, openOrdersAddressKey, openOrdersAccount, feeDiscountPubkey = null, selfTradeBehavior = 'decrementTake', programId, maxTs, replaceIfExists, } = params;
// @ts-ignore
const ownerAddress = owner.publicKey ?? owner;
return instructions_1.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_js_1.default(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(connection, { owner, baseWallet, quoteWallet, side, price, maxBaseSize, maxQuoteSize, minBaseSize, minQuoteSize, limit = 65535, programId = undefined, feeDiscountPubkey = undefined, }, feeDiscountPubkeyCacheDurationMs = 0) {
// @ts-ignore
const ownerAddress = owner.publicKey ?? owner;
const transaction = new web3_js_1.Transaction();
const signers = [];
// @ts-ignore
const vaultSigner = await web3_js_1.PublicKey.createProgramAddress([
this.address.toBuffer(),
this._decoded.vaultSignerNonce.toArrayLike(buffer_1.Buffer, 'le', 8),
], this._programId);
// Fetch an SRM fee discount key if the market supports discounts and it is not supplied
let useFeeDiscountPubkey;
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(params) {
const { owner, baseWallet, quoteWallet, vaultSigner, side, price, maxBaseSize, maxQuoteSize, minBaseSize, minQuoteSize, limit = 65535, programId, feeDiscountPubkey = null, } = params;
// @ts-ignore
const ownerAddress = owner.publicKey ?? owner;
if (this.baseSizeNumberToLots(maxBaseSize).lte(new bn_js_1.default(0))) {
throw new Error('size too small');
}
if (this.quoteSizeNumberToSplSize(maxQuoteSize).lte(new bn_js_1.default(0))) {
throw new Error('size too small');
}
if (this.priceNumberToLots(price).lte(new bn_js_1.default(0))) {
throw new Error('invalid price');
}
return instructions_1.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(accounts, orders) {
// @ts-ignore
const ownerAddress = accounts.owner.publicKey ?? accounts.owner;
return instructions_1.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_js_1.default(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,
}))
});
}
async _sendTransaction(connection, transaction, signers) {
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, owner, openOrders, clientId) {
const transaction = await this.makeCancelOrderByClientIdTransaction(connection, owner.publicKey, openOrders, clientId);
return await this._sendTransaction(connection, transaction, [owner]);
}
async cancelOrdersByClientIds(connection, owner, openOrders, clientIds) {
const transaction = await this.makeCancelOrdersByClientIdsTransaction(connection, owner.publicKey, openOrders, clientIds);
return await this._sendTransaction(connection, transaction, [owner]);
}
async makeCancelOrderByClientIdTransaction(connection, owner, openOrders, clientId) {
const transaction = new web3_js_1.Transaction();
if (this.usesRequestQueue) {
transaction.add(instructions_1.DexInstructions.cancelOrderByClientId({
market: this.address,
owner,
openOrders,
requestQueue: this._decoded.requestQueue,
clientId,
programId: this._programId,
}));
}
else {
transaction.add(instructions_1.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, owner, openOrders, clientIds) {
const transaction = new web3_js_1.Transaction();
transaction.add(instructions_1.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, owner, order) {
const transaction = await this.makeCancelOrderTransaction(connection, owner.publicKey, order);
return await this._sendTransaction(connection, transaction, [owner]);
}
async makeCancelOrderTransaction(connection, owner, order) {
const transaction = new web3_js_1.Transaction();
transaction.add(this.makeCancelOrderInstruction(connection, owner, order));
return transaction;
}
makeCancelOrderInstruction(connection, owner, order) {
if (this.usesRequestQueue) {
return instructions_1.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 instructions_1.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,
});
}
}
makeConsumeEventsInstruction(openOrdersAccounts, limit) {
return instructions_1.DexInstructions.consumeEvents({
market: this.address,
eventQueue: this._decoded.eventQueue,
coinFee: this._decoded.eventQueue,
pcFee: this._decoded.eventQueue,
openOrdersAccounts,
limit,
programId: this._programId,
});
}
makeConsumeEventsPermissionedInstruction(openOrdersAccounts, limit) {
return instructions_1.DexInstructions.consumeEventsPermissioned({
market: this.address,
eventQueue: this._decoded.eventQueue,
crankAuthority: this._decoded.consumeEventsAuthority,
openOrdersAccounts,
limit,
programId: this._programId,
});
}
async settleFunds(connection, owner, openOrders, baseWallet, quoteWallet, referrerQuoteWallet = 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, openOrders, baseWallet, quoteWallet, referrerQuoteWallet = null) {
// @ts-ignore
const vaultSigner = await web3_js_1.PublicKey.createProgramAddress([
this.address.toBuffer(),
this._decoded.vaultSignerNonce.toArrayLike(buffer_1.Buffer, 'le', 8),
], this._programId);
const transaction = new web3_js_1.Transaction();
const signers = [];
let wrappedSolAccount = null;
if ((this.baseMintAddress.equals(token_instructions_1.WRAPPED_SOL_MINT) &&
baseWallet.equals(openOrders.owner)) ||
(this.quoteMintAddress.equals(token_instructions_1.WRAPPED_SOL_MINT) &&
quoteWallet.equals(openOrders.owner))) {
wrappedSolAccount = new web3_js_1.Account();
transaction.add(web3_js_1.SystemProgram.createAccount({
fromPubkey: openOrders.owner,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: token_instructions_1.TOKEN_PROGRAM_ID,
}));
transaction.add((0, token_instructions_1.initializeAccount)({
account: wrappedSolAccount.publicKey,
mint: token_instructions_1.WRAPPED_SOL_MINT,
owner: openOrders.owner,
}));
signers.push(wrappedSolAccount);
}
transaction.add(instructions_1.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((0, token_instructions_1.closeAccount)({
source: wrappedSolAccount.publicKey,
destination: openOrders.owner,
owner: openOrders.owner,
}));
}
return { transaction, signers, payer: openOrders.owner };
}
async matchOrders(connection, feePayer, limit) {
const tx = this.makeMatchOrdersTransaction(limit);
return await this._sendTransaction(connection, tx, [feePayer]);
}
makeMatchOrdersTransaction(limit) {
const tx = new web3_js_1.Transaction();
tx.add(instructions_1.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) {
const { data } = throwIfNull(await connection.getAccountInfo(this._decoded.requestQueue));
return (0, queue_1.decodeRequestQueue)(data);
}
async loadEventQueue(connection) {
const { data } = throwIfNull(await connection.getAccountInfo(this._decoded.eventQueue));
return (0, queue_1.decodeEventQueue)(data);
}
async loadFills(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 = (0, queue_1.decodeEventQueue)(data, limit);
return events
.filter((event) => event.eventFlags.fill && event.nativeQuantityPaid.gtn(0))
.map(this.parseFillEvent.bind(this));
}
parseFillEvent(event) {
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,
};
}
get _baseSplTokenMultiplier() {
return new bn_js_1.default(10).pow(new bn_js_1.default(this._baseSplTokenDecimals));
}
get _quoteSplTokenMultiplier() {
return new bn_js_1.default(10).pow(new bn_js_1.default(this._quoteSplTokenDecimals));
}
priceLotsToNumber(price) {
return divideBnToNumber(price.mul(this._decoded.quoteLotSize).mul(this._baseSplTokenMultiplier), this._decoded.baseLotSize.mul(this._quoteSplTokenMultiplier));
}
priceNumberToLots(price) {
return new bn_js_1.default(Math.round((price *
Math.pow(10, this._quoteSplTokenDecimals) *
this._decoded.baseLotSize.toNumber()) /
(Math.pow(10, this._baseSplTokenDecimals) *
this._decoded.quoteLotSize.toNumber())));
}
baseSplSizeToNumber(size) {
return divideBnToNumber(size, this._baseSplTokenMultiplier);
}
quoteSplSizeToNumber(size) {
return divideBnToNumber(size, this._quoteSplTokenMultiplier);
}
baseSizeNumberToSplSize(size) {
return new bn_js_1.default(Math.round(size * Math.pow(10, this._baseSplTokenDecimals)));
}
quoteSizeNumberToSplSize(size) {
return new bn_js_1.default(Math.round(size * Math.pow(10, this._quoteSplTokenDecimals)));
}
baseSizeLotsToNumber(size) {
return divideBnToNumber(size.mul(this._decoded.baseLotSize), this._baseSplTokenMultiplier);
}
baseSizeNumberToLots(size) {
const native = new bn_js_1.default(Math.round(size * Math.pow(10, this._baseSplTokenDecimals)));
// rounds down to the nearest lot size
return native.div(this._decoded.baseLotSize);
}
quoteSizeLotsToNumber(size) {
return divideBnToNumber(size.mul(this._decoded.quoteLotSize), this._quoteSplTokenMultiplier);
}
quoteSizeNumberToLots(size) {
const native = new bn_js_1.default(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_js_1.default(1));
}
get tickSize() {
return this.priceLotsToNumber(new bn_js_1.default(1));
}
}
exports.Market = Market;
exports._OPEN_ORDERS_LAYOUT_V1 = (0, buffer_layout_1.struct)([
(0, buffer_layout_1.blob)(5),
(0, layout_1.accountFlagsLayout)('accountFlags'),
(0, layout_1.publicKeyLayout)('market'),
(0, layout_1.publicKeyLayout)('owner'),
// These are in spl-token (i.e. not lot) units
(0, layout_1.u64)('baseTokenFree'),
(0, layout_1.u64)('baseTokenTotal'),
(0, layout_1.u64)('quoteTokenFree'),
(0, layout_1.u64)('quoteTokenTotal'),
(0, layout_1.u128)('freeSlotBits'),
(0, layout_1.u128)('isBidBits'),
(0, buffer_layout_1.seq)((0, layout_1.u128)(), 128, 'orders'),
(0, buffer_layout_1.seq)((0, layout_1.u64)(), 128, 'clientIds'),
(0, buffer_layout_1.blob)(7),
]);
exports._OPEN_ORDERS_LAYOUT_V2 = (0, buffer_layout_1.struct)([
(0, buffer_layout_1.blob)(5),
(0, layout_1.accountFlagsLayout)('accountFlags'),
(0, layout_1.publicKeyLayout)('market'),
(0, layout_1.publicKeyLayout)('owner'),
// These are in spl-token (i.e. not lot) units
(0, layout_1.u64)('baseTokenFree'),
(0, layout_1.u64)('baseTokenTotal'),
(0, layout_1.u64)('quoteTokenFree'),
(0, layout_1.u64)('quoteTokenTotal'),
(0, layout_1.u128)('freeSlotBits'),
(0, layout_1.u128)('isBidBits'),
(0, buffer_layout_1.seq)((0, layout_1.u128)(), 128, 'orders'),
(0, buffer_layout_1.seq)((0, layout_1.u64)(), 128, 'clientIds'),
(0, layout_1.u64)('referrerRebatesAccrued'),
(0, buffer_layout_1.blob)(7),
]);
class OpenOrders {
_programId;
address;
market;
owner;
baseTokenFree;
baseTokenTotal;
quoteTokenFree;
quoteTokenTotal;
freeSlotBits;
isBidBits;
orders;
clientIds;
constructor(address, decoded, programId) {
this.address = address;
this._programId = programId;
Object.assign(this, decoded);
}
static getLayout(programId) {
if ((0, tokens_and_markets_1.getLayoutVersion)(programId) === 1) {
return exports._OPEN_ORDERS_LAYOUT_V1;
}
return exports._OPEN_ORDERS_LAYOUT_V2;
}
static async findForOwner(connection, ownerAddress, programId) {
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, marketAddress, ownerAddress, programId) {
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, address, programId) {
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, accountInfo, programId) {
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, marketAddress, ownerAddress, newAccountAddress, programId) {
return web3_js_1.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;
}
}
exports.OpenOrders = OpenOrders;
exports.ORDERBOOK_LAYOUT = (0, buffer_layout_1.struct)([
(0, buffer_layout_1.blob)(5),
(0, layout_1.accountFlagsLayout)('accountFlags'),
slab_1.SLAB_LAYOUT.replicate('slab'),
(0, buffer_layout_1.blob)(7),
]);
class Orderbook {
market;
isBids;
slab;
constructor(market, accountFlags, 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 exports.ORDERBOOK_LAYOUT;
}
static decode(market, buffer) {
const { accountFlags, slab } = exports.ORDERBOOK_LAYOUT.decode(buffer);
return new Orderbook(market, accountFlags, slab);
}
getL2(depth) {
const descending = this.isBids;
const levels = []; // (price, size)
for (const { key, quantity } of this.slab.items(descending)) {
const price = getPriceFromKey(key);
if (levels.length > 0 && levels[levels.length - 1][0].eq(price)) {
levels[levels.length - 1][1] = levels[levels.length - 1][1].add(quantity);
}
else if (levels.length === depth) {
break;
}
else {
levels.push([price, quantity]);
}
}
return levels.map(([priceLots, sizeLots]) => [
this.market.priceLotsToNumber(priceLots),
this.market.baseSizeLotsToNumber(sizeLots),
priceLots,
sizeLots,
]);
}
[Symbol.iterator]() {
return this.items(false);
}
*items(descending = false) {
for (const { key, ownerSlot, owner, quantity, feeTier, clientOrderId, } of this.slab.items(descending)) {
const price = getPriceFromKey(key);
yield {
orderId: key,
clientId: clientOrderId,
openOrdersAddress: owner,
openOrdersSlot: ownerSlot,
feeTier,
price: this.market.priceLotsToNumber(price),
priceLots: price,
si