@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
1,159 lines • 50.6 kB
JavaScript
import { PublicKey, Keypair, SystemProgram, Transaction, sendAndConfirmTransaction, } from '@solana/web3.js';
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, unpackMint, } from '@solana/spl-token';
import { createCreateMarketInstruction, createGlobalAddTraderInstruction, createGlobalCreateInstruction, createGlobalDepositInstruction, createGlobalWithdrawInstruction, createSwapInstruction, createBatchUpdateInstruction as createBatchUpdateCoreInstruction, } from './manifest/instructions';
import { OrderType } from './manifest/types';
import { Market } from './market';
import { Wrapper } from './wrapperObj';
import { PROGRAM_ID as MANIFEST_PROGRAM_ID, PROGRAM_ID } from './manifest';
import { PROGRAM_ID as WRAPPER_PROGRAM_ID, createBatchUpdateBaseGlobalInstruction, createBatchUpdateInstruction, createBatchUpdateQuoteGlobalInstruction, createClaimSeatInstruction, createCreateWrapperInstruction, createDepositInstruction, createWithdrawInstruction, } from './wrapper';
import { FIXED_WRAPPER_HEADER_SIZE, NO_EXPIRATION_LAST_VALID_SLOT, } from './constants';
import { getVaultAddress } from './utils/market';
import { genAccDiscriminator } from './utils/discriminator';
import { getGlobalAddress, getGlobalVaultAddress } from './utils/global';
import { Global } from './global';
const marketDiscriminator = genAccDiscriminator('manifest::state::market::MarketFixed');
export class ManifestClient {
connection;
wrapper;
market;
payer;
baseMint;
quoteMint;
baseGlobal;
quoteGlobal;
isBase22;
isQuote22;
constructor(connection, wrapper, market, payer, baseMint, quoteMint,
// Globals are public. The expectation is that users will directly access
// them, similar to the market.
baseGlobal, quoteGlobal) {
this.connection = connection;
this.wrapper = wrapper;
this.market = market;
this.payer = payer;
this.baseMint = baseMint;
this.quoteMint = quoteMint;
this.baseGlobal = baseGlobal;
this.quoteGlobal = quoteGlobal;
// If no extension data then the mint is not Token2022
this.isBase22 = baseMint.tlvData.length > 0;
this.isQuote22 = quoteMint.tlvData.length > 0;
}
/**
* fetches all user wrapper accounts and returns the first or null if none are found
*
* @param connection Connection
* @param payerPub PublicKey of the trader
*
* @returns Promise<GetProgramAccountsResponse>
*/
static async fetchFirstUserWrapper(connection, payerPub) {
const existingWrappers = await connection.getProgramAccounts(WRAPPER_PROGRAM_ID, {
filters: [
// Dont check discriminant since there is only one type of account.
{
memcmp: {
offset: 8,
encoding: 'base58',
bytes: payerPub.toBase58(),
},
},
],
});
return existingWrappers.length > 0 ? existingWrappers[0] : null;
}
/**
* list all Manifest markets using getProgramAccounts. caution: this is a heavy call.
*
* @param connection Connection
* @returns PublicKey[]
*/
static async listMarketPublicKeys(connection) {
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
dataSlice: { offset: 0, length: 0 },
filters: [
{
memcmp: {
offset: 0,
bytes: marketDiscriminator.toString('base64'),
encoding: 'base64',
},
},
],
});
return accounts.map((a) => a.pubkey);
}
/**
* List all Manifest markets that match base and quote mint. If useApi, then
* this call uses the manifest stats server instead of the heavy
* getProgramAccounts RPC call.
*
* @param connection Connection
* @param baseMint PublicKey
* @param quoteMint PublicKey
* @param useApi boolean
* @returns PublicKey[]
*/
static async listMarketsForMints(connection, baseMint, quoteMint, useApi) {
if (useApi) {
const responseJson = await (await fetch('https://mfx-stats-mainnet.fly.dev/tickers')).json();
const tickers = responseJson
.filter((ticker) => {
return (ticker.base_currency == baseMint.toBase58() &&
ticker.target_currency == quoteMint.toBase58());
})
.map((ticker) => {
return new PublicKey(ticker.ticker_id);
});
return tickers;
}
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
dataSlice: { offset: 0, length: 0 },
filters: [
{
memcmp: {
offset: 0,
bytes: marketDiscriminator.toString('base64'),
encoding: 'base64',
},
},
{
memcmp: {
offset: 16,
bytes: baseMint.toBase58(),
encoding: 'base58',
},
},
{
memcmp: {
offset: 48,
bytes: quoteMint.toBase58(),
encoding: 'base58',
},
},
],
});
return accounts.map((a) => a.pubkey);
}
/**
* Get all market program accounts. This is expensive RPC load..
*
* @param connection Connection
* @returns GetProgramAccountsResponse
*/
static async getMarketProgramAccounts(connection) {
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{
memcmp: {
offset: 0,
bytes: marketDiscriminator.toString('base64'),
encoding: 'base64',
},
},
],
});
return accounts;
}
/**
* Create a new client which creates a wrapper and claims seat if needed.
*
* @param connection Connection
* @param marketPk PublicKey of the market
* @param payerKeypair Keypair of the trader
*
* @returns ManifestClient
*/
static async getClientForMarket(connection, marketPk, payerKeypair) {
const marketObject = await Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk = marketObject.baseMint();
const quoteMintPk = marketObject.quoteMint();
const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk));
const baseMint = unpackMint(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk));
const quoteMint = unpackMint(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner);
const baseGlobal = await Global.loadFromAddress({
connection,
address: getGlobalAddress(baseMint.address),
});
const quoteGlobal = await Global.loadFromAddress({
connection,
address: getGlobalAddress(quoteMint.address),
});
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, payerKeypair.publicKey);
const transaction = new Transaction();
if (!userWrapper) {
const wrapperKeypair = Keypair.generate();
const createAccountIx = SystemProgram.createAccount({
fromPubkey: payerKeypair.publicKey,
newAccountPubkey: wrapperKeypair.publicKey,
space: FIXED_WRAPPER_HEADER_SIZE,
lamports: await connection.getMinimumBalanceForRentExemption(FIXED_WRAPPER_HEADER_SIZE),
programId: WRAPPER_PROGRAM_ID,
});
const createWrapperIx = createCreateWrapperInstruction({
owner: payerKeypair.publicKey,
wrapperState: wrapperKeypair.publicKey,
});
const claimSeatIx = createClaimSeatInstruction({
manifestProgram: MANIFEST_PROGRAM_ID,
owner: payerKeypair.publicKey,
market: marketPk,
wrapperState: wrapperKeypair.publicKey,
});
transaction.add(createAccountIx);
transaction.add(createWrapperIx);
transaction.add(claimSeatIx);
await sendAndConfirmTransaction(connection, transaction, [
payerKeypair,
wrapperKeypair,
]);
const wrapper = await Wrapper.loadFromAddress({
connection,
address: wrapperKeypair.publicKey,
});
return new ManifestClient(connection, wrapper, marketObject, payerKeypair.publicKey, baseMint, quoteMint, baseGlobal, quoteGlobal);
}
// Otherwise there is an existing wrapper
const wrapperData = Wrapper.deserializeWrapperBuffer(userWrapper.account.data);
const existingMarketInfos = wrapperData.marketInfos.filter((marketInfo) => {
return marketInfo.market.toBase58() == marketPk.toBase58();
});
if (existingMarketInfos.length > 0) {
const wrapper = await Wrapper.loadFromAddress({
connection,
address: userWrapper.pubkey,
});
return new ManifestClient(connection, wrapper, marketObject, payerKeypair.publicKey, baseMint, quoteMint, baseGlobal, quoteGlobal);
}
// There is a wrapper, but need to claim a seat.
const claimSeatIx = createClaimSeatInstruction({
manifestProgram: MANIFEST_PROGRAM_ID,
owner: payerKeypair.publicKey,
market: marketPk,
wrapperState: userWrapper.pubkey,
});
transaction.add(claimSeatIx);
await sendAndConfirmTransaction(connection, transaction, [payerKeypair]);
const wrapper = await Wrapper.loadFromAddress({
connection,
address: userWrapper.pubkey,
});
return new ManifestClient(connection, wrapper, marketObject, payerKeypair.publicKey, baseMint, quoteMint, baseGlobal, quoteGlobal);
}
/**
* generate ixs which need to be executed in order to run a manifest client for a given market. `{ setupNeeded: false }` means all good.
* this function should be used before getClientForMarketNoPrivateKey for UI cases where `Keypair`s cannot be directly passed in.
*
* @param connection Connection
* @param marketPk PublicKey of the market
* @param trader PublicKey of the trader
*
* @returns Promise<SetupData>
*/
static async getSetupIxs(connection, marketPk, trader) {
const setupData = {
setupNeeded: true,
instructions: [],
wrapperKeypair: null,
};
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader);
if (!userWrapper) {
const wrapperKeypair = Keypair.generate();
setupData.wrapperKeypair = wrapperKeypair;
const createAccountIx = SystemProgram.createAccount({
fromPubkey: trader,
newAccountPubkey: wrapperKeypair.publicKey,
space: FIXED_WRAPPER_HEADER_SIZE,
lamports: await connection.getMinimumBalanceForRentExemption(FIXED_WRAPPER_HEADER_SIZE),
programId: WRAPPER_PROGRAM_ID,
});
setupData.instructions.push(createAccountIx);
const createWrapperIx = createCreateWrapperInstruction({
owner: trader,
wrapperState: wrapperKeypair.publicKey,
});
setupData.instructions.push(createWrapperIx);
const claimSeatIx = createClaimSeatInstruction({
manifestProgram: MANIFEST_PROGRAM_ID,
owner: trader,
market: marketPk,
wrapperState: wrapperKeypair.publicKey,
});
setupData.instructions.push(claimSeatIx);
return setupData;
}
const wrapperData = Wrapper.deserializeWrapperBuffer(userWrapper.account.data);
const existingMarketInfos = wrapperData.marketInfos.filter((marketInfo) => {
return marketInfo.market.toBase58() == marketPk.toBase58();
});
if (existingMarketInfos.length > 0) {
setupData.setupNeeded = false;
return setupData;
}
// There is a wrapper, but need to claim a seat.
const claimSeatIx = createClaimSeatInstruction({
manifestProgram: MANIFEST_PROGRAM_ID,
owner: trader,
market: marketPk,
wrapperState: userWrapper.pubkey,
});
setupData.instructions.push(claimSeatIx);
return setupData;
}
/**
* Create a new client. throws if setup ixs are needed. Call ManifestClient.getSetupIxs to check if ixs are needed.
* This is the way to create a client without directly passing in `Keypair` types (for example when building a UI).
*
* @param connection Connection
* @param marketPk PublicKey of the market
* @param trader PublicKey of the trader
*
* @returns ManifestClient
*/
static async getClientForMarketNoPrivateKey(connection, marketPk, trader) {
const { setupNeeded } = await this.getSetupIxs(connection, marketPk, trader);
if (setupNeeded) {
throw new Error('setup ixs need to be executed first');
}
const marketObject = await Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk = marketObject.baseMint();
const quoteMintPk = marketObject.quoteMint();
const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk));
const baseMint = unpackMint(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk));
const quoteMint = unpackMint(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner);
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader);
if (!userWrapper) {
throw new Error('userWrapper is null even though setupNeeded is false. This should never happen.');
}
const wrapper = await Wrapper.loadFromAddress({
connection,
address: userWrapper.pubkey,
});
const baseGlobal = await Global.loadFromAddress({
connection,
address: getGlobalAddress(baseMint.address),
});
const quoteGlobal = await Global.loadFromAddress({
connection,
address: getGlobalAddress(quoteMint.address),
});
return new ManifestClient(connection, wrapper, marketObject, trader, baseMint, quoteMint, baseGlobal, quoteGlobal);
}
/**
* Create a new client that is read only. Cannot send transactions or generate instructions.
*
* @param connection Connection
* @param marketPk PublicKey of the market
* @param trader PublicKey for trader whose wrapper to fetch
*
* @returns ManifestClient
*/
static async getClientReadOnly(connection, marketPk, trader) {
const marketObject = await Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk = marketObject.baseMint();
const quoteMintPk = marketObject.quoteMint();
const baseGlobalPk = getGlobalAddress(baseMintPk);
const quoteGlobalPk = getGlobalAddress(quoteMintPk);
const [baseMintAccountInfo, quoteMintAccountInfo, baseGlobalAccountInfo, quoteGlobalAccountInfo,] = await connection.getMultipleAccountsInfo([
baseMintPk,
quoteMintPk,
baseGlobalPk,
quoteGlobalPk,
]);
const baseMint = unpackMint(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMint = unpackMint(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner);
// Global accounts are optional
const baseGlobal = baseGlobalAccountInfo &&
Global.loadFromBuffer({
address: baseGlobalPk,
buffer: baseGlobalAccountInfo.data,
});
const quoteGlobal = quoteGlobalAccountInfo &&
Global.loadFromBuffer({
address: quoteGlobalPk,
buffer: quoteGlobalAccountInfo.data,
});
let wrapper = null;
if (trader != null) {
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader);
if (userWrapper) {
wrapper = Wrapper.loadFromBuffer({
address: userWrapper.pubkey,
buffer: userWrapper.account.data,
});
}
}
return new ManifestClient(connection, wrapper, marketObject, null, baseMint, quoteMint, baseGlobal, quoteGlobal);
}
/**
* Initializes a ReadOnlyClient for each Market the trader has a seat on.
* This has been optimized to be as light on the RPC as possible but it is
* still using getProgramAccounts. caution: this is a heavy call.
*
* @param connection Connection
* @param trader PublicKey
* @returns ManifestClient[]
*/
static async getClientsReadOnlyForAllTraderSeats(connection, trader) {
const marketAccountResponse = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{
memcmp: {
offset: 0,
bytes: marketDiscriminator.toString('base64'),
encoding: 'base64',
},
},
],
withContext: true,
});
const markets = marketAccountResponse.value.map((m) => Market.loadFromBuffer({
address: m.pubkey,
buffer: m.account.data,
slot: marketAccountResponse.context.slot,
}));
const marketsForTrader = markets.filter((m) => m.hasSeat(trader));
const baseMintPks = marketsForTrader.map((m) => m.baseMint().toString());
const quoteMintPks = marketsForTrader.map((m) => m.quoteMint().toString());
const baseGlobalPks = marketsForTrader.map((m) => getGlobalAddress(m.baseMint()).toString());
const quoteGlobalPks = marketsForTrader.map((m) => getGlobalAddress(m.quoteMint()).toString());
// ensure every account is only fetched once
const allAisFetched = {};
const allPksToFetch = [
...new Set([
...baseMintPks,
...quoteMintPks,
...baseGlobalPks,
...quoteGlobalPks,
]),
];
const mutableCopy = Array.from(allPksToFetch);
while (mutableCopy.length > 0) {
const batchPks = mutableCopy.splice(0, 100);
const batchAis = await connection.getMultipleAccountsInfoAndContext(batchPks.map((a) => new PublicKey(a)));
batchAis.value.forEach((ai, i) => (allAisFetched[batchPks[i]] = ai));
}
let wrapper = null;
if (trader != null) {
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader);
if (userWrapper) {
wrapper = Wrapper.loadFromBuffer({
address: userWrapper.pubkey,
buffer: userWrapper.account.data,
});
}
}
return marketsForTrader.map((m, i) => {
const baseMintAccountInfo = allAisFetched[baseMintPks[i]];
const quoteMintAccountInfo = allAisFetched[quoteMintPks[i]];
const baseGlobalAccountInfo = allAisFetched[baseGlobalPks[i]];
const quoteGlobalAccountInfo = allAisFetched[quoteGlobalPks[i]];
const baseMint = unpackMint(m.baseMint(), baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMint = unpackMint(m.quoteMint(), quoteMintAccountInfo, quoteMintAccountInfo.owner);
// Global accounts are optional
const baseGlobal = baseGlobalAccountInfo &&
Global.loadFromBuffer({
address: new PublicKey(baseGlobalPks[i]),
buffer: baseGlobalAccountInfo.data,
});
const quoteGlobal = quoteGlobalAccountInfo &&
Global.loadFromBuffer({
address: new PublicKey(quoteGlobalPks[i]),
buffer: quoteGlobalAccountInfo.data,
});
return new ManifestClient(connection, wrapper, m, null, baseMint, quoteMint, baseGlobal, quoteGlobal);
});
}
/**
* Reload the market and wrapper and global objects.
*/
async reload() {
await Promise.all([
() => {
if (this.wrapper) {
return this.wrapper.reload(this.connection);
}
},
() => {
if (this.baseGlobal) {
return this.baseGlobal.reload(this.connection);
}
},
() => {
if (this.quoteGlobal) {
return this.quoteGlobal.reload(this.connection);
}
},
this.market.reload(this.connection),
]);
}
/**
* CreateMarket instruction. Assumes the account is already funded onchain.
*
* @param payer PublicKey of the trader
* @param baseMint PublicKey of the baseMint
* @param quoteMint PublicKey of the quoteMint
* @param market PublicKey of the market that will be created. Private key
* will need to be a signer.
*
* @returns TransactionInstruction
*/
static createMarketIx(payer, baseMint, quoteMint, market) {
const baseVault = getVaultAddress(market, baseMint);
const quoteVault = getVaultAddress(market, quoteMint);
return createCreateMarketInstruction({
payer,
market,
baseVault,
quoteVault,
baseMint,
quoteMint,
tokenProgram22: TOKEN_2022_PROGRAM_ID,
});
}
/**
* Deposit instruction
*
* @param payer PublicKey of the trader
* @param mint PublicKey for deposit mint. Must be either the base or quote
* @param amountTokens Number of tokens to deposit.
*
* @returns TransactionInstruction
*/
depositIx(payer, mint, amountTokens) {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const vault = getVaultAddress(this.market.address, mint);
const is22 = (mint.equals(this.baseMint.address) && this.isBase22) ||
(mint.equals(this.quoteMint.address) && this.isQuote22);
const traderTokenAccount = getAssociatedTokenAddressSync(mint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID);
const mintDecimals = this.market.quoteMint().toBase58() === mint.toBase58()
? this.market.quoteDecimals()
: this.market.baseDecimals();
const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals);
return createDepositInstruction({
market: this.market.address,
traderTokenAccount,
vault,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
mint,
tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
}, {
params: {
amountAtoms,
},
});
}
/**
* Withdraw instruction
*
* @param payer PublicKey of the trader
* @param mint PublicKey for withdraw mint. Must be either the base or quote
* @param amountTokens Number of tokens to withdraw.
*
* @returns TransactionInstruction
*/
withdrawIx(payer, mint, amountTokens) {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const vault = getVaultAddress(this.market.address, mint);
const is22 = (mint.equals(this.baseMint.address) && this.isBase22) ||
(mint.equals(this.quoteMint.address) && this.isQuote22);
const traderTokenAccount = getAssociatedTokenAddressSync(mint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID);
const mintDecimals = this.market.quoteMint().toBase58() === mint.toBase58()
? this.market.quoteDecimals()
: this.market.baseDecimals();
const amountAtoms = Math.floor(amountTokens * 10 ** mintDecimals);
return createWithdrawInstruction({
market: this.market.address,
traderTokenAccount,
vault,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
mint,
tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
}, {
params: {
amountAtoms,
},
});
}
/**
* Withdraw All instruction. Withdraws all available base and quote tokens
*
* @returns TransactionInstruction[]
*/
withdrawAllIx() {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const withdrawInstructions = [];
const baseBalance = this.market.getWithdrawableBalanceTokens(this.payer, true);
if (baseBalance > 0) {
const baseWithdrawIx = this.withdrawIx(this.payer, this.market.baseMint(), baseBalance);
withdrawInstructions.push(baseWithdrawIx);
}
const quoteBalance = this.market.getWithdrawableBalanceTokens(this.payer, false);
if (quoteBalance > 0) {
const quoteWithdrawIx = this.withdrawIx(this.payer, this.market.quoteMint(), quoteBalance);
withdrawInstructions.push(quoteWithdrawIx);
}
return withdrawInstructions;
}
/**
* PlaceOrder instruction
*
* @param params WrapperPlaceOrderParamsExternal | WrapperPlaceOrderReverseParamsExternal
* including all the information for placing an order like amount, price,
* ordertype, ... This is called external because to avoid conflicts with the
* autogenerated version which has problems with expressing some of the
* parameters. The reverse type has a spreadBps field instead of lastValidSlot.
*
* @returns TransactionInstruction
*/
placeOrderIx(params) {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
if (params.orderType != OrderType.Global) {
return createBatchUpdateInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
}, {
params: {
cancels: [],
cancelAll: false,
orders: [toWrapperPlaceOrderParams(this.market, params)],
},
});
}
if (params.isBid) {
const global = getGlobalAddress(this.quoteMint.address);
const globalVault = getGlobalVaultAddress(this.quoteMint.address);
const vault = getVaultAddress(this.market.address, this.quoteMint.address);
return createBatchUpdateQuoteGlobalInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
quoteMint: this.quoteMint.address,
quoteGlobal: global,
quoteGlobalVault: globalVault,
quoteMarketVault: vault,
quoteTokenProgram: this.isQuote22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
}, {
params: {
cancels: [],
cancelAll: false,
orders: [toWrapperPlaceOrderParams(this.market, params)],
},
});
}
else {
const global = getGlobalAddress(this.baseMint.address);
const globalVault = getGlobalVaultAddress(this.baseMint.address);
const vault = getVaultAddress(this.market.address, this.baseMint.address);
return createBatchUpdateBaseGlobalInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
baseMint: this.baseMint.address,
baseGlobal: global,
baseGlobalVault: globalVault,
baseMarketVault: vault,
baseTokenProgram: this.isBase22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
}, {
params: {
cancels: [],
cancelAll: false,
orders: [toWrapperPlaceOrderParams(this.market, params)],
},
});
}
}
/**
* PlaceOrderWithRequiredDeposit instruction. Only deposits the appropriate base
* or quote tokens if not in the withdrawable balances.
*
* @param payer PublicKey of the trader
* @param params WrapperPlaceOrderParamsExternal | WrapperPlaceOrderReverseParamsExternal
* including all the information for placing an order like amount, price,
* ordertype, ... This is called external because to avoid conflicts with the
* autogenerated version which has problems with expressing some of the
* parameters. The reverse type has a spreadBps field instead of lastValidSlot.
*
* @returns TransactionInstruction[]
*/
async placeOrderWithRequiredDepositIxs(payer, params) {
const placeOrderIx = this.placeOrderIx(params);
if (params.orderType != OrderType.Global) {
const currentBalanceTokens = this.market.getWithdrawableBalanceTokens(payer, !params.isBid);
let depositMint;
let depositAmountTokens = 0;
if (params.isBid) {
depositMint = this.market.quoteMint();
depositAmountTokens =
params.numBaseTokens * params.tokenPrice - currentBalanceTokens;
}
else {
depositMint = this.market.baseMint();
depositAmountTokens = params.numBaseTokens - currentBalanceTokens;
}
if (depositAmountTokens <= 0) {
return [placeOrderIx];
}
const depositIx = this.depositIx(payer, depositMint, depositAmountTokens);
return [depositIx, placeOrderIx];
}
else {
const global = (params.isBid ? this.quoteGlobal : this.baseGlobal);
const currentBalanceTokens = await global.getGlobalBalanceTokens(this.connection, payer);
let depositMint;
let depositAmountTokens = 0;
if (params.isBid) {
depositMint = this.market.quoteMint();
depositAmountTokens =
params.numBaseTokens * params.tokenPrice - currentBalanceTokens;
}
else {
depositMint = this.market.baseMint();
depositAmountTokens = params.numBaseTokens - currentBalanceTokens;
}
if (depositAmountTokens <= 0) {
return [placeOrderIx];
}
const depositIx = await ManifestClient.globalDepositIx(this.connection, payer, depositMint, depositAmountTokens);
return [depositIx, placeOrderIx];
}
}
/**
* Swap instruction
*
* Optimized swap for routers and arb bots. Normal traders should compose
* depost/withdraw/placeOrder to get limit orders. Does not go through the
* wrapper.
*
* @param payer PublicKey of the trader
* @param params SwapParams
*
* @returns TransactionInstruction
*/
swapIx(payer, params) {
const traderBase = getAssociatedTokenAddressSync(this.baseMint.address, payer, true, this.isBase22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID);
const traderQuote = getAssociatedTokenAddressSync(this.quoteMint.address, payer, true, this.isQuote22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID);
const baseVault = getVaultAddress(this.market.address, this.baseMint.address);
const quoteVault = getVaultAddress(this.market.address, this.quoteMint.address);
const global = getGlobalAddress(params.isBaseIn ? this.quoteMint.address : this.baseMint.address);
const globalVault = getGlobalVaultAddress(params.isBaseIn ? this.quoteMint.address : this.baseMint.address);
// Assumes just normal token program for now.
// No Token22 support here in sdk yet, but includes programs and mints as
// though it was.
// No support for the case where global are not needed. That is an
// optimization that needs to be made when looking at the orderbook and
// deciding if it is worthwhile to lock the accounts.
return createSwapInstruction({
payer,
market: this.market.address,
traderBase,
traderQuote,
baseVault,
quoteVault,
tokenProgramBase: this.isBase22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
baseMint: this.baseMint.address,
tokenProgramQuote: this.isQuote22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
quoteMint: this.quoteMint.address,
global,
globalVault,
}, {
params,
});
}
/**
* CancelOrder instruction
*
* @param params WrapperCancelOrderParams includes the clientOrderId of the
* order to cancel.
*
* @returns TransactionInstruction
*/
cancelOrderIx(params) {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
// Global not required for cancels. If we do cancel a global, then our gas
// prepayment is abandoned.
return createBatchUpdateInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
}, {
params: {
cancels: [params],
cancelAll: false,
orders: [],
},
});
}
/**
* BatchUpdate instruction
*
* @param placeParams (WrapperPlaceOrderParamsExternal | WrapperPlaceOrderReverseParamsExternal)[]
* including all the information for placing an order like amount, price,
* ordertype, ... This is called external because to avoid conflicts with the
* autogenerated version which has problems with expressing some of the
* parameters. The reverse type has a spreadBps field instead of lastValidSlot.
* @param params WrapperCancelOrderParams[] includes the clientOrderId of the
* order to cancel.
*
* @returns TransactionInstruction
*/
batchUpdateIx(placeParams, cancelParams, cancelAll) {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const baseGlobalRequired = placeParams.some((placeParams) => {
return !placeParams.isBid && placeParams.orderType == OrderType.Global;
});
const quoteGlobalRequired = placeParams.some((placeParams) => {
return placeParams.isBid && placeParams.orderType == OrderType.Global;
});
if (!baseGlobalRequired && !quoteGlobalRequired) {
return createBatchUpdateInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
}, {
params: {
cancels: cancelParams,
cancelAll,
orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)),
},
});
}
if (!baseGlobalRequired && quoteGlobalRequired) {
const global = getGlobalAddress(this.quoteMint.address);
const globalVault = getGlobalVaultAddress(this.quoteMint.address);
const vault = getVaultAddress(this.market.address, this.quoteMint.address);
return createBatchUpdateQuoteGlobalInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
quoteMint: this.quoteMint.address,
quoteGlobal: global,
quoteGlobalVault: globalVault,
quoteTokenProgram: this.isQuote22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
quoteMarketVault: vault,
}, {
params: {
cancels: cancelParams,
cancelAll,
orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)),
},
});
}
if (baseGlobalRequired && !quoteGlobalRequired) {
const global = getGlobalAddress(this.baseMint.address);
const globalVault = getGlobalVaultAddress(this.baseMint.address);
const vault = getVaultAddress(this.market.address, this.baseMint.address);
return createBatchUpdateBaseGlobalInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
baseMint: this.baseMint.address,
baseGlobal: global,
baseGlobalVault: globalVault,
baseTokenProgram: this.isBase22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
baseMarketVault: vault,
}, {
params: {
cancels: cancelParams,
cancelAll,
orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)),
},
});
}
const baseGlobal = getGlobalAddress(this.baseMint.address);
const baseGlobalVault = getGlobalVaultAddress(this.baseMint.address);
const baseMarketVault = getVaultAddress(this.market.address, this.baseMint.address);
const quoteGlobal = getGlobalAddress(this.quoteMint.address);
const quoteGlobalVault = getGlobalVaultAddress(this.quoteMint.address);
const quoteMarketVault = getVaultAddress(this.market.address, this.quoteMint.address);
return createBatchUpdateInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
baseMint: this.baseMint.address,
baseGlobal,
baseGlobalVault,
baseTokenProgram: this.isBase22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
baseMarketVault,
quoteMint: this.quoteMint.address,
quoteGlobal,
quoteGlobalVault,
quoteTokenProgram: this.isQuote22
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
quoteMarketVault,
}, {
params: {
cancels: cancelParams,
cancelAll,
orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)),
},
});
}
/**
* CancelAll instruction. Cancels all orders on a market. This is discouraged
* outside of circuit breaker usage because it is less efficient and does not
* cancel global cleanly. Use batchUpdate instead. This also does not cancel
* any orders not placed through the wrapper, which includes reverse orders
* that were reversed.
*
* @returns TransactionInstruction
*/
cancelAllIx() {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
// Global not required for cancelAll. If we do cancel a global, then our gas
// prepayment is abandoned.
return createBatchUpdateInstruction({
market: this.market.address,
manifestProgram: MANIFEST_PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
}, {
params: {
cancels: [],
cancelAll: true,
orders: [],
},
});
}
/**
* CancelAllOnCore instruction. Cancels all orders on a market directly on the core program,
* including reverse orders and global orders with rent prepayment.
*
* @returns TransactionInstruction[]
*/
async cancelAllOnCoreIx() {
if (!this.payer) {
throw new Error('Read only');
}
const openOrders = this.market.openOrders();
const ordersToCancel = [];
for (const openOrder of openOrders) {
if (openOrder.trader.toBase58() === this.payer.toBase58()) {
const seqNum = openOrder.sequenceNumber;
ordersToCancel.push({
orderSequenceNumber: seqNum,
orderIndexHint: null,
});
}
}
const MAX_CANCELS_PER_BATCH = 25;
const cancelInstructions = [];
for (let i = 0; i < ordersToCancel.length; i += MAX_CANCELS_PER_BATCH) {
const batchOfCancels = ordersToCancel.slice(i, i + MAX_CANCELS_PER_BATCH);
const batchedCancelInstruction = createBatchUpdateCoreInstruction({
payer: this.payer,
market: this.market.address,
}, {
params: {
cancels: batchOfCancels,
orders: [],
traderIndexHint: null,
},
});
cancelInstructions.push(batchedCancelInstruction);
}
return cancelInstructions;
}
/**
* killSwitchMarket transactions. Pulls all orders
* and withdraws all balances from the market in two transactions
*
* @param payer PublicKey of the trader
*
* @returns TransactionSignatures[]
*/
async killSwitchMarket(payerKeypair) {
await this.market.reload(this.connection);
const cancelAllIx = this.cancelAllIx();
const cancelAllTx = new Transaction();
const cancelAllSig = await sendAndConfirmTransaction(this.connection, cancelAllTx.add(cancelAllIx), [payerKeypair], {
skipPreflight: true,
commitment: 'confirmed',
});
// TOOD: Merge this into one transaction
await this.market.reload(this.connection);
const withdrawAllIx = this.withdrawAllIx();
const withdrawAllTx = new Transaction();
const withdrawAllSig = await sendAndConfirmTransaction(this.connection, withdrawAllTx.add(...withdrawAllIx), [payerKeypair], {
skipPreflight: true,
commitment: 'confirmed',
});
return [cancelAllSig, withdrawAllSig];
}
/**
* CreateGlobalCreate instruction. Creates the global account. Should be used only once per mint.
*
* @param connection Connection to pull mint info
* @param payer PublicKey of the trader
* @param globalMint PublicKey of the globalMint
*
* @returns Promise<TransactionInstruction>
*/
static async createGlobalCreateIx(connection, payer, globalMint) {
const global = getGlobalAddress(globalMint);
const globalVault = getGlobalVaultAddress(globalMint);
const globalMintAccountInfo = (await connection.getAccountInfo(globalMint));
const mint = unpackMint(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner);
const is22 = mint.tlvData.length > 0;
return createGlobalCreateInstruction({
payer,
global,
mint: globalMint,
globalVault,
tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
});
}
/**
* CreateGlobalAddTrader instruction. Adds a new trader to the global account.
* Static because it does not require a wrapper.
*
* @param payer PublicKey of the trader
* @param globalMint PublicKey of the globalMint
*
* @returns TransactionInstruction
*/
static createGlobalAddTraderIx(payer, globalMint) {
const global = getGlobalAddress(globalMint);
return createGlobalAddTraderInstruction({
payer,
global,
});
}
/**
* Global deposit instruction. Static because it does not require a wrapper.
*
* @param connection Connection to pull mint info
* @param payer PublicKey of the trader
* @param globalMint PublicKey for global mint deposit.
* @param amountTokens Number of tokens to deposit.
*
* @returns Promise<TransactionInstruction>
*/
static async globalDepositIx(connection, payer, globalMint, amountTokens) {
const globalAddress = getGlobalAddress(globalMint);
const globalVault = getGlobalVaultAddress(globalMint);
const globalMintAccountInfo = (await connection.getAccountInfo(globalMint));
const mint = unpackMint(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner);
const is22 = mint.tlvData.length > 0;
const traderTokenAccount = getAssociatedTokenAddressSync(globalMint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID);
const mintDecimals = mint.decimals;
const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals);
return createGlobalDepositInstruction({
payer: payer,
global: globalAddress,
mint: globalMint,
globalVault: globalVault,
traderToken: traderTokenAccount,
tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
}, {
params: {
amountAtoms,
},
});
}
/**
* Global withdraw instruction. Static because it does not require a wrapper.
*
* @param connection Connection to pull mint info
* @param payer PublicKey of the trader
* @param globalMint PublicKey for global mint withdraw.
* @param amountTokens Number of tokens to withdraw.
*
* @returns Promise<TransactionInstruction>
*/
static async globalWithdrawIx(connection, payer, globalMint, amountTokens) {
const globalAddress = getGlobalAddress(globalMint);
const globalVault = getGlobalVaultAddress(globalMint);
const globalMintAccountInfo = (await connection.getAccountInfo(globalMint));
const mint = unpackMint(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner);
const is22 = mint.tlvData.length > 0;
const traderTokenAccount = getAssociatedTokenAddressSync(globalMint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID);
const mintDecimals = mint.decimals;
const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals);
return createGlobalWithdrawInstruction({
payer: payer,
global: globalAddress,
mint: globalMint,
globalVault: globalVault,
traderToken: traderTokenAccount,
tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
}, {
params: {
amountAtoms,
},
});
}
}
function toWrapperPlaceOrderParams(market, wrapperPlaceOrderParamsExternal) {
// Convert spread bps to 10^-5.
if ('spreadBps' in wrapperPlaceOrderParamsExternal) {
wrapperPlaceOrderParamsExternal['lastValidSlot'] = Math.floor(wrapperPlaceOrderParamsExternal['spreadBps'] * 10);
}
else if (wrapperPlaceOrderParamsExternal['lastValidSlot'] < 100_000 &&
wrapperPlaceOrderParamsExternal['lastValidSlot'] !=
NO_EXPIRATION_LAST_VALID_SLOT) {
// 100_000 is way earlier than the current slot. This check ensures that
// users are intentionally choosing the right type.
throw new Error('Last valid slot on order not valid');
}
const quoteAtomsPerToken = 10 ** market.quoteDecimals();
const baseAtomsPerToken = 10 ** market.baseDecimals();
// Converts token price to atom price since not always equal
// Ex. BONK/USDC = 0.00001854 USDC tokens/BONK tokens -> 0.0001854 USDC Atoms/BONK Atoms
const priceQuoteAtomsPerBaseAtoms = wrapperPlaceOrderParamsExternal.tokenPrice *
(quoteAtomsPerToken / baseAtomsPerToken);
const { priceMantissa, priceExponent } = toMantissaAndExponent(priceQuoteAtomsPerBaseAtoms);
const numBaseAtoms = Mat