@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
1,671 lines (1,577 loc) • 54.7 kB
text/typescript
import { bignum } from '@metaplex-foundation/beet';
import {
PublicKey,
Connection,
Keypair,
TransactionInstruction,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
AccountInfo,
TransactionSignature,
GetProgramAccountsResponse,
} from '@solana/web3.js';
import {
Mint,
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, SwapParams } from './manifest/types';
import { Market, RestingOrder } from './market';
import { WrapperMarketInfo, Wrapper, WrapperData } from './wrapperObj';
import { PROGRAM_ID as MANIFEST_PROGRAM_ID, PROGRAM_ID } from './manifest';
import {
PROGRAM_ID as WRAPPER_PROGRAM_ID,
WrapperCancelOrderParams,
WrapperPlaceOrderParams,
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';
export interface SetupData {
setupNeeded: boolean;
instructions: TransactionInstruction[];
wrapperKeypair: Keypair | null;
}
type WrapperResponse = Readonly<{
account: AccountInfo<Buffer>;
pubkey: PublicKey;
}>;
const marketDiscriminator: Buffer = genAccDiscriminator(
'manifest::state::market::MarketFixed',
);
export class ManifestClient {
public isBase22: boolean;
public isQuote22: boolean;
private constructor(
public connection: Connection,
public wrapper: Wrapper | null,
public market: Market,
private payer: PublicKey | null,
private baseMint: Mint,
private quoteMint: Mint,
// Globals are public. The expectation is that users will directly access
// them, similar to the market.
public baseGlobal: Global | null,
public quoteGlobal: Global | null,
) {
// 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>
*/
private static async fetchFirstUserWrapper(
connection: Connection,
payerPub: PublicKey,
): Promise<WrapperResponse | null> {
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[]
*/
public static async listMarketPublicKeys(
connection: Connection,
): Promise<PublicKey[]> {
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[]
*/
public static async listMarketsForMints(
connection: Connection,
baseMint: PublicKey,
quoteMint: PublicKey,
useApi?: boolean,
): Promise<PublicKey[]> {
if (useApi) {
const responseJson = await (
await fetch('https://mfx-stats-mainnet.fly.dev/tickers')
).json();
const tickers: PublicKey[] = 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
*/
public static async getMarketProgramAccounts(
connection: Connection,
): Promise<GetProgramAccountsResponse> {
const accounts: GetProgramAccountsResponse =
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
*/
public static async getClientForMarket(
connection: Connection,
marketPk: PublicKey,
payerKeypair: Keypair,
): Promise<ManifestClient> {
const marketObject: Market = await Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk: PublicKey = marketObject.baseMint();
const quoteMintPk: PublicKey = marketObject.quoteMint();
const baseMintAccountInfo: AccountInfo<Buffer> =
(await connection.getAccountInfo(baseMintPk))!;
const baseMint: Mint = unpackMint(
baseMintPk,
baseMintAccountInfo,
baseMintAccountInfo.owner,
);
const quoteMintAccountInfo: AccountInfo<Buffer> =
(await connection.getAccountInfo(quoteMintPk))!;
const quoteMint: Mint = unpackMint(
quoteMintPk,
quoteMintAccountInfo,
quoteMintAccountInfo.owner,
);
const baseGlobal: Global | null = await Global.loadFromAddress({
connection,
address: getGlobalAddress(baseMint.address),
});
const quoteGlobal: Global | null = await Global.loadFromAddress({
connection,
address: getGlobalAddress(quoteMint.address),
});
const userWrapper = await ManifestClient.fetchFirstUserWrapper(
connection,
payerKeypair.publicKey,
);
const transaction: Transaction = new Transaction();
if (!userWrapper) {
const wrapperKeypair: Keypair = Keypair.generate();
const createAccountIx: TransactionInstruction =
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: TransactionInstruction =
createCreateWrapperInstruction({
owner: payerKeypair.publicKey,
wrapperState: wrapperKeypair.publicKey,
});
const claimSeatIx: TransactionInstruction = 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: WrapperData = Wrapper.deserializeWrapperBuffer(
userWrapper.account.data,
);
const existingMarketInfos: WrapperMarketInfo[] =
wrapperData.marketInfos.filter((marketInfo: WrapperMarketInfo) => {
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: TransactionInstruction = 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>
*/
public static async getSetupIxs(
connection: Connection,
marketPk: PublicKey,
trader: PublicKey,
): Promise<SetupData> {
const setupData: SetupData = {
setupNeeded: true,
instructions: [],
wrapperKeypair: null,
};
const userWrapper = await ManifestClient.fetchFirstUserWrapper(
connection,
trader,
);
if (!userWrapper) {
const wrapperKeypair: Keypair = Keypair.generate();
setupData.wrapperKeypair = wrapperKeypair;
const createAccountIx: TransactionInstruction =
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: TransactionInstruction =
createCreateWrapperInstruction({
owner: trader,
wrapperState: wrapperKeypair.publicKey,
});
setupData.instructions.push(createWrapperIx);
const claimSeatIx: TransactionInstruction = createClaimSeatInstruction({
manifestProgram: MANIFEST_PROGRAM_ID,
owner: trader,
market: marketPk,
wrapperState: wrapperKeypair.publicKey,
});
setupData.instructions.push(claimSeatIx);
return setupData;
}
const wrapperData: WrapperData = Wrapper.deserializeWrapperBuffer(
userWrapper.account.data,
);
const existingMarketInfos: WrapperMarketInfo[] =
wrapperData.marketInfos.filter((marketInfo: WrapperMarketInfo) => {
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: TransactionInstruction = 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
*/
public static async getClientForMarketNoPrivateKey(
connection: Connection,
marketPk: PublicKey,
trader: PublicKey,
): Promise<ManifestClient> {
const { setupNeeded } = await this.getSetupIxs(
connection,
marketPk,
trader,
);
if (setupNeeded) {
throw new Error('setup ixs need to be executed first');
}
const marketObject: Market = await Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk: PublicKey = marketObject.baseMint();
const quoteMintPk: PublicKey = marketObject.quoteMint();
const baseMintAccountInfo: AccountInfo<Buffer> =
(await connection.getAccountInfo(baseMintPk))!;
const baseMint: Mint = unpackMint(
baseMintPk,
baseMintAccountInfo,
baseMintAccountInfo.owner,
);
const quoteMintAccountInfo: AccountInfo<Buffer> =
(await connection.getAccountInfo(quoteMintPk))!;
const quoteMint: Mint = 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: Global | null = await Global.loadFromAddress({
connection,
address: getGlobalAddress(baseMint.address),
});
const quoteGlobal: Global | null = 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
*/
public static async getClientReadOnly(
connection: Connection,
marketPk: PublicKey,
trader?: PublicKey,
): Promise<ManifestClient> {
const marketObject: Market = await Market.loadFromAddress({
connection: connection,
address: marketPk,
});
const baseMintPk: PublicKey = marketObject.baseMint();
const quoteMintPk: PublicKey = marketObject.quoteMint();
const baseGlobalPk: PublicKey = getGlobalAddress(baseMintPk);
const quoteGlobalPk: PublicKey = getGlobalAddress(quoteMintPk);
const [
baseMintAccountInfo,
quoteMintAccountInfo,
baseGlobalAccountInfo,
quoteGlobalAccountInfo,
]: (AccountInfo<Buffer> | null)[] =
await connection.getMultipleAccountsInfo([
baseMintPk,
quoteMintPk,
baseGlobalPk,
quoteGlobalPk,
]);
const baseMint: Mint = unpackMint(
baseMintPk,
baseMintAccountInfo,
baseMintAccountInfo!.owner,
);
const quoteMint: Mint = unpackMint(
quoteMintPk,
quoteMintAccountInfo,
quoteMintAccountInfo!.owner,
);
// Global accounts are optional
const baseGlobal: Global | null =
baseGlobalAccountInfo &&
Global.loadFromBuffer({
address: baseGlobalPk,
buffer: baseGlobalAccountInfo.data,
});
const quoteGlobal: Global | null =
quoteGlobalAccountInfo &&
Global.loadFromBuffer({
address: quoteGlobalPk,
buffer: quoteGlobalAccountInfo.data,
});
let wrapper: Wrapper | null = null;
if (trader != null) {
const userWrapper: WrapperResponse | null =
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[]
*/
public static async getClientsReadOnlyForAllTraderSeats(
connection: Connection,
trader: PublicKey,
): Promise<ManifestClient[]> {
const marketAccountResponse = await connection.getProgramAccounts(
PROGRAM_ID,
{
filters: [
{
memcmp: {
offset: 0,
bytes: marketDiscriminator.toString('base64'),
encoding: 'base64',
},
},
],
withContext: true,
},
);
const markets: Market[] = marketAccountResponse.value.map((m) =>
Market.loadFromBuffer({
address: m.pubkey,
buffer: m.account.data,
slot: marketAccountResponse.context.slot,
}),
);
const marketsForTrader: Market[] = markets.filter((m) => m.hasSeat(trader));
const baseMintPks: string[] = marketsForTrader.map((m) =>
m.baseMint().toString(),
);
const quoteMintPks: string[] = marketsForTrader.map((m) =>
m.quoteMint().toString(),
);
const baseGlobalPks: string[] = marketsForTrader.map((m) =>
getGlobalAddress(m.baseMint()).toString(),
);
const quoteGlobalPks: string[] = marketsForTrader.map((m) =>
getGlobalAddress(m.quoteMint()).toString(),
);
// ensure every account is only fetched once
const allAisFetched: { [pk: string]: AccountInfo<Buffer> | null } = {};
const allPksToFetch: string[] = [
...new Set([
...baseMintPks,
...quoteMintPks,
...baseGlobalPks,
...quoteGlobalPks,
]),
];
const mutableCopy = Array.from(allPksToFetch);
while (mutableCopy.length > 0) {
const batchPks: string[] = 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: Wrapper | null = null;
if (trader != null) {
const userWrapper: WrapperResponse | null =
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: Mint = unpackMint(
m.baseMint(),
baseMintAccountInfo,
baseMintAccountInfo!.owner,
);
const quoteMint: Mint = unpackMint(
m.quoteMint(),
quoteMintAccountInfo,
quoteMintAccountInfo!.owner,
);
// Global accounts are optional
const baseGlobal: Global | null =
baseGlobalAccountInfo &&
Global.loadFromBuffer({
address: new PublicKey(baseGlobalPks[i]),
buffer: baseGlobalAccountInfo.data,
});
const quoteGlobal: Global | null =
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.
*/
public async reload(): Promise<void> {
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
*/
private static createMarketIx(
payer: PublicKey,
baseMint: PublicKey,
quoteMint: PublicKey,
market: PublicKey,
): TransactionInstruction {
const baseVault: PublicKey = getVaultAddress(market, baseMint);
const quoteVault: PublicKey = 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
*/
public depositIx(
payer: PublicKey,
mint: PublicKey,
amountTokens: number,
): TransactionInstruction {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const vault: PublicKey = getVaultAddress(this.market.address, mint);
const is22: boolean =
(mint.equals(this.baseMint.address) && this.isBase22) ||
(mint.equals(this.quoteMint.address) && this.isQuote22);
const traderTokenAccount: PublicKey = 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
*/
public withdrawIx(
payer: PublicKey,
mint: PublicKey,
amountTokens: number,
): TransactionInstruction {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const vault: PublicKey = getVaultAddress(this.market.address, mint);
const is22: boolean =
(mint.equals(this.baseMint.address) && this.isBase22) ||
(mint.equals(this.quoteMint.address) && this.isQuote22);
const traderTokenAccount: PublicKey = 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[]
*/
public withdrawAllIx(): TransactionInstruction[] {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const withdrawInstructions: TransactionInstruction[] = [];
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
*/
public placeOrderIx(
params:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
): TransactionInstruction {
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: PublicKey = getGlobalAddress(this.quoteMint.address);
const globalVault: PublicKey = getGlobalVaultAddress(
this.quoteMint.address,
);
const vault: PublicKey = 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: PublicKey = getGlobalAddress(this.baseMint.address);
const globalVault: PublicKey = getGlobalVaultAddress(
this.baseMint.address,
);
const vault: PublicKey = 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[]
*/
public async placeOrderWithRequiredDepositIxs(
payer: PublicKey,
params:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
): Promise<TransactionInstruction[]> {
const placeOrderIx: TransactionInstruction = this.placeOrderIx(params);
if (params.orderType != OrderType.Global) {
const currentBalanceTokens: number =
this.market.getWithdrawableBalanceTokens(payer, !params.isBid);
let depositMint: PublicKey;
let depositAmountTokens: number = 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: Global = (
params.isBid ? this.quoteGlobal : this.baseGlobal
)!;
const currentBalanceTokens: number = await global.getGlobalBalanceTokens(
this.connection,
payer,
);
let depositMint: PublicKey;
let depositAmountTokens: number = 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
*/
public swapIx(payer: PublicKey, params: SwapParams): TransactionInstruction {
const traderBase: PublicKey = getAssociatedTokenAddressSync(
this.baseMint.address,
payer,
true,
this.isBase22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
);
const traderQuote: PublicKey = getAssociatedTokenAddressSync(
this.quoteMint.address,
payer,
true,
this.isQuote22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
);
const baseVault: PublicKey = getVaultAddress(
this.market.address,
this.baseMint.address,
);
const quoteVault: PublicKey = getVaultAddress(
this.market.address,
this.quoteMint.address,
);
const global: PublicKey = getGlobalAddress(
params.isBaseIn ? this.quoteMint.address : this.baseMint.address,
);
const globalVault: PublicKey = 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
*/
public cancelOrderIx(
params: WrapperCancelOrderParams,
): TransactionInstruction {
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
*/
public batchUpdateIx(
placeParams: (
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal
)[],
cancelParams: WrapperCancelOrderParams[],
cancelAll: boolean,
): TransactionInstruction {
if (!this.wrapper || !this.payer) {
throw new Error('Read only');
}
const baseGlobalRequired: boolean = placeParams.some(
(
placeParams:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
) => {
return !placeParams.isBid && placeParams.orderType == OrderType.Global;
},
);
const quoteGlobalRequired: boolean = placeParams.some(
(
placeParams:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
) => {
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:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
) => toWrapperPlaceOrderParams(this.market, params),
),
},
},
);
}
if (!baseGlobalRequired && quoteGlobalRequired) {
const global: PublicKey = getGlobalAddress(this.quoteMint.address);
const globalVault: PublicKey = getGlobalVaultAddress(
this.quoteMint.address,
);
const vault: PublicKey = 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:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
) => toWrapperPlaceOrderParams(this.market, params),
),
},
},
);
}
if (baseGlobalRequired && !quoteGlobalRequired) {
const global: PublicKey = getGlobalAddress(this.baseMint.address);
const globalVault: PublicKey = getGlobalVaultAddress(
this.baseMint.address,
);
const vault: PublicKey = 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:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
) => toWrapperPlaceOrderParams(this.market, params),
),
},
},
);
}
const baseGlobal: PublicKey = getGlobalAddress(this.baseMint.address);
const baseGlobalVault: PublicKey = getGlobalVaultAddress(
this.baseMint.address,
);
const baseMarketVault: PublicKey = getVaultAddress(
this.market.address,
this.baseMint.address,
);
const quoteGlobal: PublicKey = getGlobalAddress(this.quoteMint.address);
const quoteGlobalVault: PublicKey = getGlobalVaultAddress(
this.quoteMint.address,
);
const quoteMarketVault: PublicKey = 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:
| WrapperPlaceOrderParamsExternal
| WrapperPlaceOrderReverseParamsExternal,
) => 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
*/
public cancelAllIx(): TransactionInstruction {
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[]
*/
public async cancelAllOnCoreIx(): Promise<TransactionInstruction[]> {
if (!this.payer) {
throw new Error('Read only');
}
const openOrders: RestingOrder[] = this.market.openOrders();
const ordersToCancel: {
orderSequenceNumber: bignum;
orderIndexHint: null;
}[] = [];
for (const openOrder of openOrders) {
if (openOrder.trader.toBase58() === this.payer.toBase58()) {
const seqNum: bignum = openOrder.sequenceNumber;
ordersToCancel.push({
orderSequenceNumber: seqNum,
orderIndexHint: null,
});
}
}
const MAX_CANCELS_PER_BATCH = 25;
const cancelInstructions: TransactionInstruction[] = [];
for (let i = 0; i < ordersToCancel.length; i += MAX_CANCELS_PER_BATCH) {
const batchOfCancels = ordersToCancel.slice(i, i + MAX_CANCELS_PER_BATCH);
const batchedCancelInstruction: TransactionInstruction =
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[]
*/
public async killSwitchMarket(
payerKeypair: Keypair,
): Promise<TransactionSignature[]> {
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>
*/
private static async createGlobalCreateIx(
connection: Connection,
payer: PublicKey,
globalMint: PublicKey,
): Promise<TransactionInstruction> {
const global: PublicKey = getGlobalAddress(globalMint);
const globalVault: PublicKey = getGlobalVaultAddress(globalMint);
const globalMintAccountInfo: AccountInfo<Buffer> =
(await connection.getAccountInfo(globalMint))!;
const mint: Mint = unpackMint(
globalMint,
globalMintAccountInfo,
globalMintAccountInfo.owner,
);
const is22: boolean = 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
*/
public static createGlobalAddTraderIx(
payer: PublicKey,
globalMint: PublicKey,
): TransactionInstruction {
const global: PublicKey = 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>
*/
public static async globalDepositIx(
connection: Connection,
payer: PublicKey,
globalMint: PublicKey,
amountTokens: number,
): Promise<TransactionInstruction> {
const globalAddress: PublicKey = getGlobalAddress(globalMint);
const globalVault: PublicKey = getGlobalVaultAddress(globalMint);
const globalMintAccountInfo: AccountInfo<Buffer> =
(await connection.getAccountInfo(globalMint))!;
const mint: Mint = unpackMint(
globalMint,
globalMintAccountInfo,
globalMintAccountInfo.owner,
);
const is22: boolean = mint.tlvData.length > 0;
const traderTokenAccount: PublicKey = 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 with