@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
1,129 lines • 52.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManifestClient = void 0;
exports.toMantissaAndExponent = toMantissaAndExponent;
const web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
const instructions_1 = require("./manifest/instructions");
const types_1 = require("./manifest/types");
const market_1 = require("./market");
const wrapperObj_1 = require("./wrapperObj");
const manifest_1 = require("./manifest");
const wrapper_1 = require("./wrapper");
const constants_1 = require("./constants");
const market_2 = require("./utils/market");
const discriminator_1 = require("./utils/discriminator");
const global_1 = require("./utils/global");
const global_2 = require("./global");
const marketDiscriminator = (0, discriminator_1.genAccDiscriminator)('manifest::state::market::MarketFixed');
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_1.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(manifest_1.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 web3_js_1.PublicKey(ticker.ticker_id);
});
return tickers;
}
const accounts = await connection.getProgramAccounts(manifest_1.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(manifest_1.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_1.Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk = marketObject.baseMint();
const quoteMintPk = marketObject.quoteMint();
const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk));
const baseMint = (0, spl_token_1.unpackMint)(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk));
const quoteMint = (0, spl_token_1.unpackMint)(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner);
const baseGlobal = await global_2.Global.loadFromAddress({
connection,
address: (0, global_1.getGlobalAddress)(baseMint.address),
});
const quoteGlobal = await global_2.Global.loadFromAddress({
connection,
address: (0, global_1.getGlobalAddress)(quoteMint.address),
});
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, payerKeypair.publicKey);
const transaction = new web3_js_1.Transaction();
if (!userWrapper) {
const wrapperKeypair = web3_js_1.Keypair.generate();
const createAccountIx = web3_js_1.SystemProgram.createAccount({
fromPubkey: payerKeypair.publicKey,
newAccountPubkey: wrapperKeypair.publicKey,
space: constants_1.FIXED_WRAPPER_HEADER_SIZE,
lamports: await connection.getMinimumBalanceForRentExemption(constants_1.FIXED_WRAPPER_HEADER_SIZE),
programId: wrapper_1.PROGRAM_ID,
});
const createWrapperIx = (0, wrapper_1.createCreateWrapperInstruction)({
owner: payerKeypair.publicKey,
wrapperState: wrapperKeypair.publicKey,
});
const claimSeatIx = (0, wrapper_1.createClaimSeatInstruction)({
manifestProgram: manifest_1.PROGRAM_ID,
owner: payerKeypair.publicKey,
market: marketPk,
wrapperState: wrapperKeypair.publicKey,
});
transaction.add(createAccountIx);
transaction.add(createWrapperIx);
transaction.add(claimSeatIx);
await (0, web3_js_1.sendAndConfirmTransaction)(connection, transaction, [
payerKeypair,
wrapperKeypair,
]);
const wrapper = await wrapperObj_1.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 = wrapperObj_1.Wrapper.deserializeWrapperBuffer(userWrapper.account.data);
const existingMarketInfos = wrapperData.marketInfos.filter((marketInfo) => {
return marketInfo.market.toBase58() == marketPk.toBase58();
});
if (existingMarketInfos.length > 0) {
const wrapper = await wrapperObj_1.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 = (0, wrapper_1.createClaimSeatInstruction)({
manifestProgram: manifest_1.PROGRAM_ID,
owner: payerKeypair.publicKey,
market: marketPk,
wrapperState: userWrapper.pubkey,
});
transaction.add(claimSeatIx);
await (0, web3_js_1.sendAndConfirmTransaction)(connection, transaction, [payerKeypair]);
const wrapper = await wrapperObj_1.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 = web3_js_1.Keypair.generate();
setupData.wrapperKeypair = wrapperKeypair;
const createAccountIx = web3_js_1.SystemProgram.createAccount({
fromPubkey: trader,
newAccountPubkey: wrapperKeypair.publicKey,
space: constants_1.FIXED_WRAPPER_HEADER_SIZE,
lamports: await connection.getMinimumBalanceForRentExemption(constants_1.FIXED_WRAPPER_HEADER_SIZE),
programId: wrapper_1.PROGRAM_ID,
});
setupData.instructions.push(createAccountIx);
const createWrapperIx = (0, wrapper_1.createCreateWrapperInstruction)({
owner: trader,
wrapperState: wrapperKeypair.publicKey,
});
setupData.instructions.push(createWrapperIx);
const claimSeatIx = (0, wrapper_1.createClaimSeatInstruction)({
manifestProgram: manifest_1.PROGRAM_ID,
owner: trader,
market: marketPk,
wrapperState: wrapperKeypair.publicKey,
});
setupData.instructions.push(claimSeatIx);
return setupData;
}
const wrapperData = wrapperObj_1.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 = (0, wrapper_1.createClaimSeatInstruction)({
manifestProgram: manifest_1.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_1.Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk = marketObject.baseMint();
const quoteMintPk = marketObject.quoteMint();
const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk));
const baseMint = (0, spl_token_1.unpackMint)(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk));
const quoteMint = (0, spl_token_1.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 wrapperObj_1.Wrapper.loadFromAddress({
connection,
address: userWrapper.pubkey,
});
const baseGlobal = await global_2.Global.loadFromAddress({
connection,
address: (0, global_1.getGlobalAddress)(baseMint.address),
});
const quoteGlobal = await global_2.Global.loadFromAddress({
connection,
address: (0, global_1.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_1.Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk = marketObject.baseMint();
const quoteMintPk = marketObject.quoteMint();
const baseGlobalPk = (0, global_1.getGlobalAddress)(baseMintPk);
const quoteGlobalPk = (0, global_1.getGlobalAddress)(quoteMintPk);
const [baseMintAccountInfo, quoteMintAccountInfo, baseGlobalAccountInfo, quoteGlobalAccountInfo,] = await connection.getMultipleAccountsInfo([
baseMintPk,
quoteMintPk,
baseGlobalPk,
quoteGlobalPk,
]);
const baseMint = (0, spl_token_1.unpackMint)(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMint = (0, spl_token_1.unpackMint)(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner);
// Global accounts are optional
const baseGlobal = baseGlobalAccountInfo &&
global_2.Global.loadFromBuffer({
address: baseGlobalPk,
buffer: baseGlobalAccountInfo.data,
});
const quoteGlobal = quoteGlobalAccountInfo &&
global_2.Global.loadFromBuffer({
address: quoteGlobalPk,
buffer: quoteGlobalAccountInfo.data,
});
let wrapper = null;
if (trader != null) {
const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader);
if (userWrapper) {
wrapper = wrapperObj_1.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(manifest_1.PROGRAM_ID, {
filters: [
{
memcmp: {
offset: 0,
bytes: marketDiscriminator.toString('base64'),
encoding: 'base64',
},
},
],
withContext: true,
});
const markets = marketAccountResponse.value.map((m) => market_1.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) => (0, global_1.getGlobalAddress)(m.baseMint()).toString());
const quoteGlobalPks = marketsForTrader.map((m) => (0, global_1.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 web3_js_1.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 = wrapperObj_1.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 = (0, spl_token_1.unpackMint)(m.baseMint(), baseMintAccountInfo, baseMintAccountInfo.owner);
const quoteMint = (0, spl_token_1.unpackMint)(m.quoteMint(), quoteMintAccountInfo, quoteMintAccountInfo.owner);
// Global accounts are optional
const baseGlobal = baseGlobalAccountInfo &&
global_2.Global.loadFromBuffer({
address: new web3_js_1.PublicKey(baseGlobalPks[i]),
buffer: baseGlobalAccountInfo.data,
});
const quoteGlobal = quoteGlobalAccountInfo &&
global_2.Global.loadFromBuffer({
address: new web3_js_1.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 = (0, market_2.getVaultAddress)(market, baseMint);
const quoteVault = (0, market_2.getVaultAddress)(market, quoteMint);
return (0, instructions_1.createCreateMarketInstruction)({
payer,
market,
baseVault,
quoteVault,
baseMint,
quoteMint,
tokenProgram22: spl_token_1.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 = (0, market_2.getVaultAddress)(this.market.address, mint);
const is22 = (mint.equals(this.baseMint.address) && this.isBase22) ||
(mint.equals(this.quoteMint.address) && this.isQuote22);
const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.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 (0, wrapper_1.createDepositInstruction)({
market: this.market.address,
traderTokenAccount,
vault,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
mint,
tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.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 = (0, market_2.getVaultAddress)(this.market.address, mint);
const is22 = (mint.equals(this.baseMint.address) && this.isBase22) ||
(mint.equals(this.quoteMint.address) && this.isQuote22);
const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.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 (0, wrapper_1.createWithdrawInstruction)({
market: this.market.address,
traderTokenAccount,
vault,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
mint,
tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.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 != types_1.OrderType.Global) {
return (0, wrapper_1.createBatchUpdateInstruction)({
market: this.market.address,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
}, {
params: {
cancels: [],
cancelAll: false,
orders: [toWrapperPlaceOrderParams(this.market, params)],
},
});
}
if (params.isBid) {
const global = (0, global_1.getGlobalAddress)(this.quoteMint.address);
const globalVault = (0, global_1.getGlobalVaultAddress)(this.quoteMint.address);
const vault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address);
return (0, wrapper_1.createBatchUpdateQuoteGlobalInstruction)({
market: this.market.address,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
quoteMint: this.quoteMint.address,
quoteGlobal: global,
quoteGlobalVault: globalVault,
quoteMarketVault: vault,
quoteTokenProgram: this.isQuote22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.TOKEN_PROGRAM_ID,
}, {
params: {
cancels: [],
cancelAll: false,
orders: [toWrapperPlaceOrderParams(this.market, params)],
},
});
}
else {
const global = (0, global_1.getGlobalAddress)(this.baseMint.address);
const globalVault = (0, global_1.getGlobalVaultAddress)(this.baseMint.address);
const vault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address);
return (0, wrapper_1.createBatchUpdateBaseGlobalInstruction)({
market: this.market.address,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
baseMint: this.baseMint.address,
baseGlobal: global,
baseGlobalVault: globalVault,
baseMarketVault: vault,
baseTokenProgram: this.isBase22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.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 != types_1.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 = (0, spl_token_1.getAssociatedTokenAddressSync)(this.baseMint.address, payer, true, this.isBase22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID);
const traderQuote = (0, spl_token_1.getAssociatedTokenAddressSync)(this.quoteMint.address, payer, true, this.isQuote22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID);
const baseVault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address);
const quoteVault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address);
const global = (0, global_1.getGlobalAddress)(params.isBaseIn ? this.quoteMint.address : this.baseMint.address);
const globalVault = (0, global_1.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 (0, instructions_1.createSwapInstruction)({
payer,
market: this.market.address,
traderBase,
traderQuote,
baseVault,
quoteVault,
tokenProgramBase: this.isBase22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.TOKEN_PROGRAM_ID,
baseMint: this.baseMint.address,
tokenProgramQuote: this.isQuote22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.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 (0, wrapper_1.createBatchUpdateInstruction)({
market: this.market.address,
manifestProgram: manifest_1.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 == types_1.OrderType.Global;
});
const quoteGlobalRequired = placeParams.some((placeParams) => {
return placeParams.isBid && placeParams.orderType == types_1.OrderType.Global;
});
if (!baseGlobalRequired && !quoteGlobalRequired) {
return (0, wrapper_1.createBatchUpdateInstruction)({
market: this.market.address,
manifestProgram: manifest_1.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 = (0, global_1.getGlobalAddress)(this.quoteMint.address);
const globalVault = (0, global_1.getGlobalVaultAddress)(this.quoteMint.address);
const vault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address);
return (0, wrapper_1.createBatchUpdateQuoteGlobalInstruction)({
market: this.market.address,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
quoteMint: this.quoteMint.address,
quoteGlobal: global,
quoteGlobalVault: globalVault,
quoteTokenProgram: this.isQuote22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.TOKEN_PROGRAM_ID,
quoteMarketVault: vault,
}, {
params: {
cancels: cancelParams,
cancelAll,
orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)),
},
});
}
if (baseGlobalRequired && !quoteGlobalRequired) {
const global = (0, global_1.getGlobalAddress)(this.baseMint.address);
const globalVault = (0, global_1.getGlobalVaultAddress)(this.baseMint.address);
const vault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address);
return (0, wrapper_1.createBatchUpdateBaseGlobalInstruction)({
market: this.market.address,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
baseMint: this.baseMint.address,
baseGlobal: global,
baseGlobalVault: globalVault,
baseTokenProgram: this.isBase22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.TOKEN_PROGRAM_ID,
baseMarketVault: vault,
}, {
params: {
cancels: cancelParams,
cancelAll,
orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)),
},
});
}
const baseGlobal = (0, global_1.getGlobalAddress)(this.baseMint.address);
const baseGlobalVault = (0, global_1.getGlobalVaultAddress)(this.baseMint.address);
const baseMarketVault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address);
const quoteGlobal = (0, global_1.getGlobalAddress)(this.quoteMint.address);
const quoteGlobalVault = (0, global_1.getGlobalVaultAddress)(this.quoteMint.address);
const quoteMarketVault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address);
return (0, wrapper_1.createBatchUpdateInstruction)({
market: this.market.address,
manifestProgram: manifest_1.PROGRAM_ID,
owner: this.payer,
wrapperState: this.wrapper.address,
baseMint: this.baseMint.address,
baseGlobal,
baseGlobalVault,
baseTokenProgram: this.isBase22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.TOKEN_PROGRAM_ID,
baseMarketVault,
quoteMint: this.quoteMint.address,
quoteGlobal,
quoteGlobalVault,
quoteTokenProgram: this.isQuote22
? spl_token_1.TOKEN_2022_PROGRAM_ID
: spl_token_1.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 (0, wrapper_1.createBatchUpdateInstruction)({
market: this.market.address,
manifestProgram: manifest_1.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 = (0, instructions_1.createBatchUpdateInstruction)({
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 web3_js_1.Transaction();
const cancelAllSig = await (0, web3_js_1.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 web3_js_1.Transaction();
const withdrawAllSig = await (0, web3_js_1.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 = (0, global_1.getGlobalAddress)(globalMint);
const globalVault = (0, global_1.getGlobalVaultAddress)(globalMint);
const globalMintAccountInfo = (await connection.getAccountInfo(globalMint));
const mint = (0, spl_token_1.unpackMint)(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner);
const is22 = mint.tlvData.length > 0;
return (0, instructions_1.createGlobalCreateInstruction)({
payer,
global,
mint: globalMint,
globalVault,
tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.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 = (0, global_1.getGlobalAddress)(globalMint);
return (0, instructions_1.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 = (0, global_1.getGlobalAddress)(globalMint);
const globalVault = (0, global_1.getGlobalVaultAddress)(globalMint);
const globalMintAccountInfo = (await connection.getAccountInfo(globalMint));
const mint = (0, spl_token_1.unpackMint)(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner);
const is22 = mint.tlvData.length > 0;
const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(globalMint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID);
const mintDecimals = mint.decimals;
const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals);
return (0, instructions_1.createGlobalDepositInstruction)({
payer: payer,
global: globalAddress,
mint: globalMint,
globalVault: globalVault,
traderToken: traderTokenAccount,
tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.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 = (0, global_1.getGlobalAddress)(globalMint);
const globalVault = (0, global_1.getGlobalVaultAddress)(globalMint);
const globalMintAccountInfo = (await connection.getAccountInfo(globalMint));
const mint = (0, spl_token_1.unpackMint)(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner);
const is22 = mint.tlvData.length > 0;
const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(globalMint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID);
const mintDecimals = mint.decimals;
const amountAto