import { AnchorProvider, BN, Idl, Program, ProgramAccount, } from '@coral-xyz/anchor'; import bs58 from 'bs58'; import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createCloseAccountInstruction, createInitializeAccountInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; import { StateAccount, IWallet, PositionDirection, UserAccount, PerpMarketAccount, OrderParams, Order, SpotMarketAccount, SpotPosition, MakerInfo, TakerInfo, OptionalOrderParams, OrderType, ReferrerInfo, MarketType, TxParams, SerumV3FulfillmentConfigAccount, isVariant, ReferrerNameAccount, OrderTriggerCondition, SpotBalanceType, PerpMarketExtendedInfo, UserStatsAccount, ModifyOrderParams, PhoenixV1FulfillmentConfigAccount, ModifyOrderPolicy, SwapReduceOnly, } from './types'; import * as anchor from '@coral-xyz/anchor'; import driftIDL from './idl/drift.json'; import { Connection, PublicKey, TransactionSignature, ConfirmOptions, Transaction, TransactionInstruction, AccountMeta, Keypair, LAMPORTS_PER_SOL, Signer, SystemProgram, ComputeBudgetProgram, AddressLookupTableAccount, TransactionVersion, VersionedTransaction, TransactionMessage, } from '@solana/web3.js'; import { TokenFaucet } from './tokenFaucet'; import { EventEmitter } from 'events'; import StrictEventEmitter from 'strict-event-emitter-types'; import { getDriftSignerPublicKey, getDriftStateAccountPublicKey, getInsuranceFundStakeAccountPublicKey, getPerpMarketPublicKey, getPhoenixFulfillmentConfigPublicKey, getReferrerNamePublicKeySync, getSerumFulfillmentConfigPublicKey, getSerumSignerPublicKey, getSpotMarketPublicKey, getUserAccountPublicKey, getUserAccountPublicKeySync, getUserStatsAccountPublicKey, } from './addresses/pda'; import { DriftClientAccountSubscriber, DriftClientAccountEvents, DataAndSlot, DriftClientMetricsEvents, } from './accounts/types'; import { ExtraConfirmationOptions, TxSender, TxSigAndSlot } from './tx/types'; import { getSignedTransactionMap, wrapInTx } from './tx/utils'; import { BASE_PRECISION, PRICE_PRECISION, QUOTE_SPOT_MARKET_INDEX, ZERO, QUOTE_PRECISION, } from './constants/numericConstants'; import { findDirectionToClose, positionIsAvailable } from './math/position'; import { getSignedTokenAmount, getTokenAmount } from './math/spotBalance'; import { decodeName, DEFAULT_USER_NAME, encodeName } from './userName'; import { OraclePriceData } from './oracles/types'; import { DriftClientConfig } from './driftClientConfig'; import { PollingDriftClientAccountSubscriber } from './accounts/pollingDriftClientAccountSubscriber'; import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { RetryTxSender } from './tx/retryTxSender'; import { User } from './user'; import { UserSubscriptionConfig } from './userConfig'; import { configs, DRIFT_PROGRAM_ID } from './config'; import { WRAPPED_SOL_MINT } from './constants/spotMarkets'; import { UserStats } from './userStats'; import { isSpotPositionAvailable } from './math/spotPosition'; import { calculateMarketMaxAvailableInsurance } from './math/market'; import { fetchUserStatsAccount } from './accounts/fetch'; import { castNumberToSpotPrecision } from './math/spotMarket'; import { JupiterClient, QuoteResponse, Route, SwapMode, } from './jupiter/jupiterClient'; import { getNonIdleUserFilter } from './memcmp'; import { UserStatsSubscriptionConfig } from './userStatsConfig'; import { getMarinadeDepositIx, getMarinadeFinanceProgram } from './marinade'; import { getOrderParams } from './orderParams'; type RemainingAccountParams = { userAccounts: UserAccount[]; writablePerpMarketIndexes?: number[]; writableSpotMarketIndexes?: number[]; readablePerpMarketIndex?: number | number[]; readableSpotMarketIndexes?: number[]; useMarketLastSlotCache?: boolean; }; /** * # DriftClient * This class is the main way to interact with Drift Protocol. It allows you to subscribe to the various accounts where the Market's state is stored, as well as: opening positions, liquidating, settling funding, depositing & withdrawing, and more. */ export class DriftClient { connection: Connection; wallet: IWallet; public program: Program; provider: AnchorProvider; opts?: ConfirmOptions; users = new Map(); userStats?: UserStats; activeSubAccountId: number; userAccountSubscriptionConfig: UserSubscriptionConfig; userStatsAccountSubscriptionConfig: UserStatsSubscriptionConfig; accountSubscriber: DriftClientAccountSubscriber; eventEmitter: StrictEventEmitter; metricsEventEmitter: StrictEventEmitter< EventEmitter, DriftClientMetricsEvents >; _isSubscribed = false; txSender: TxSender; perpMarketLastSlotCache = new Map(); spotMarketLastSlotCache = new Map(); mustIncludePerpMarketIndexes = new Set(); mustIncludeSpotMarketIndexes = new Set(); authority: PublicKey; marketLookupTable: PublicKey; lookupTableAccount: AddressLookupTableAccount; includeDelegates?: boolean; authoritySubAccountMap?: Map; skipLoadUsers?: boolean; txVersion: TransactionVersion; txParams: TxParams; enableMetricsEvents?: boolean; public get isSubscribed() { return this._isSubscribed && this.accountSubscriber.isSubscribed; } public set isSubscribed(val: boolean) { this._isSubscribed = val; } public constructor(config: DriftClientConfig) { this.connection = config.connection; this.wallet = config.wallet; this.opts = config.opts || AnchorProvider.defaultOptions(); this.provider = new AnchorProvider( config.connection, // @ts-ignore config.wallet, this.opts ); // console.log(`DriftClient 1: wallet = ${config.wallet.publicKey.toString()}`); this.program = new Program( driftIDL as Idl, config.programID ?? new PublicKey(DRIFT_PROGRAM_ID), this.provider ); // console.log(`DriftClient 2: program = ${this.program}`); this.authority = config.authority ?? this.wallet.publicKey; this.activeSubAccountId = config.activeSubAccountId ?? 0; this.skipLoadUsers = config.skipLoadUsers ?? false; this.txVersion = config.txVersion ?? 'legacy'; this.txParams = { computeUnits: config.txParams?.computeUnits ?? 600_000, computeUnitsPrice: config.txParams?.computeUnitsPrice ?? 0, }; // console.log(`DriftClient 3: authority = ${this.authority.toString()}`); if (config.includeDelegates && config.subAccountIds) { throw new Error( 'Can only pass one of includeDelegates or subAccountIds. If you want to specify subaccount ids for multiple authorities, pass authoritySubaccountMap instead' ); } // console.log(`DriftClient 4`); if (config.authoritySubAccountMap && config.subAccountIds) { throw new Error( 'Can only pass one of authoritySubaccountMap or subAccountIds' ); } // console.log(`DriftClient 5`); if (config.authoritySubAccountMap && config.includeDelegates) { throw new Error( 'Can only pass one of authoritySubaccountMap or includeDelegates' ); } // console.log(`DriftClient 6`); this.authoritySubAccountMap = config.authoritySubAccountMap ? config.authoritySubAccountMap : config.subAccountIds ? new Map([[this.authority.toString(), config.subAccountIds]]) : new Map(); this.includeDelegates = config.includeDelegates ?? false; if (config.accountSubscription?.type === 'polling') { this.userAccountSubscriptionConfig = { type: 'polling', accountLoader: config.accountSubscription.accountLoader, }; this.userStatsAccountSubscriptionConfig = { type: 'polling', accountLoader: config.accountSubscription.accountLoader, }; // console.log(`DriftClient 7 userAccountSubscriptionConfig = ${this.userAccountSubscriptionConfig}`); // console.log(`DriftClient 8 userStatsAccountSubscriptionConfig = ${this.userStatsAccountSubscriptionConfig}`); } else { this.userAccountSubscriptionConfig = { type: 'websocket', resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, commitment: config.accountSubscription?.commitment, }; this.userStatsAccountSubscriptionConfig = { type: 'websocket', resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, commitment: config.accountSubscription?.commitment, }; } if (config.userStats) { this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: getUserStatsAccountPublicKey( this.program.programId, this.authority ), accountSubscription: this.userAccountSubscriptionConfig, }); } this.marketLookupTable = config.marketLookupTable; if (config.env && !this.marketLookupTable) { this.marketLookupTable = new PublicKey( configs[config.env].MARKET_LOOKUP_TABLE ); } const noMarketsAndOraclesSpecified = config.perpMarketIndexes === undefined && config.spotMarketIndexes === undefined && config.oracleInfos === undefined; if (config.accountSubscription?.type === 'polling') { // console.log(`DriftClient 9`); this.accountSubscriber = new PollingDriftClientAccountSubscriber( this.program, config.accountSubscription.accountLoader, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], config.oracleInfos ?? [], noMarketsAndOraclesSpecified ); // console.log(`DriftClient 10`); } else { this.accountSubscriber = new WebSocketDriftClientAccountSubscriber( this.program, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], config.oracleInfos ?? [], noMarketsAndOraclesSpecified, config.accountSubscription?.resubTimeoutMs, config.accountSubscription?.commitment ); } this.eventEmitter = this.accountSubscriber.eventEmitter; if (config.enableMetricsEvents) { this.enableMetricsEvents = true; this.metricsEventEmitter = new EventEmitter(); } // console.log(`DriftClient 11`); this.txSender = config.txSender ?? new RetryTxSender({ connection: this.connection, wallet: this.wallet, opts: this.opts, }); // console.log(`DriftClient 12`); } public getUserMapKey(subAccountId: number, authority: PublicKey): string { return `${subAccountId}_${authority.toString()}`; } createUser( subAccountId: number, accountSubscriptionConfig: UserSubscriptionConfig, authority?: PublicKey ): User { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, authority ?? this.authority, subAccountId ); return new User({ driftClient: this, userAccountPublicKey, accountSubscription: accountSubscriptionConfig, }); } public async subscribe(): Promise { let subscribePromises = [this.addAndSubscribeToUsers()].concat( this.accountSubscriber.subscribe() ); if (this.userStats !== undefined) { subscribePromises = subscribePromises.concat(this.userStats.subscribe()); } this.isSubscribed = (await Promise.all(subscribePromises)).reduce( (success, prevSuccess) => success && prevSuccess ); return this.isSubscribed; } subscribeUsers(): Promise[] { return [...this.users.values()].map((user) => user.subscribe()); } /** * Forces the accountSubscriber to fetch account updates from rpc */ public async fetchAccounts(): Promise { let promises = [...this.users.values()] .map((user) => user.fetchAccounts()) .concat(this.accountSubscriber.fetch()); if (this.userStats) { promises = promises.concat(this.userStats.fetchAccounts()); } await Promise.all(promises); } public async unsubscribe(): Promise { let unsubscribePromises = this.unsubscribeUsers().concat( this.accountSubscriber.unsubscribe() ); if (this.userStats !== undefined) { unsubscribePromises = unsubscribePromises.concat( this.userStats.unsubscribe() ); } await Promise.all(unsubscribePromises); this.isSubscribed = false; } unsubscribeUsers(): Promise[] { return [...this.users.values()].map((user) => user.unsubscribe()); } statePublicKey?: PublicKey; public async getStatePublicKey(): Promise { if (this.statePublicKey) { return this.statePublicKey; } this.statePublicKey = await getDriftStateAccountPublicKey( this.program.programId ); return this.statePublicKey; } signerPublicKey?: PublicKey; public getSignerPublicKey(): PublicKey { if (this.signerPublicKey) { return this.signerPublicKey; } this.signerPublicKey = getDriftSignerPublicKey(this.program.programId); return this.signerPublicKey; } public getStateAccount(): StateAccount { return this.accountSubscriber.getStateAccountAndSlot().data; } /** * Forces a fetch to rpc before returning accounts. Useful for anchor tests. */ public async forceGetStateAccount(): Promise { await this.accountSubscriber.fetch(); return this.accountSubscriber.getStateAccountAndSlot().data; } public getPerpMarketAccount( marketIndex: number ): PerpMarketAccount | undefined { return this.accountSubscriber.getMarketAccountAndSlot(marketIndex)?.data; } /** * Forces a fetch to rpc before returning accounts. Useful for anchor tests. * @param marketIndex */ public async forceGetPerpMarketAccount( marketIndex: number ): Promise { await this.accountSubscriber.fetch(); let data = this.accountSubscriber.getMarketAccountAndSlot(marketIndex)?.data; let i = 0; while (data === undefined && i < 10) { await this.accountSubscriber.fetch(); data = this.accountSubscriber.getMarketAccountAndSlot(marketIndex)?.data; i++; } return data; } public getPerpMarketAccounts(): PerpMarketAccount[] { return this.accountSubscriber .getMarketAccountsAndSlots() .filter((value) => value !== undefined) .map((value) => value.data); } public getSpotMarketAccount( marketIndex: number ): SpotMarketAccount | undefined { return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } /** * Forces a fetch to rpc before returning accounts. Useful for anchor tests. * @param marketIndex */ public async forceGetSpotMarketAccount( marketIndex: number ): Promise { await this.accountSubscriber.fetch(); return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } public getSpotMarketAccounts(): SpotMarketAccount[] { return this.accountSubscriber .getSpotMarketAccountsAndSlots() .filter((value) => value !== undefined) .map((value) => value.data); } public getQuoteSpotMarketAccount(): SpotMarketAccount { return this.accountSubscriber.getSpotMarketAccountAndSlot( QUOTE_SPOT_MARKET_INDEX ).data; } public getOraclePriceDataAndSlot( oraclePublicKey: PublicKey ): DataAndSlot | undefined { return this.accountSubscriber.getOraclePriceDataAndSlot(oraclePublicKey); } public async getSerumV3FulfillmentConfig( serumMarket: PublicKey ): Promise { const address = await getSerumFulfillmentConfigPublicKey( this.program.programId, serumMarket ); return (await this.program.account.serumV3FulfillmentConfig.fetch( address )) as SerumV3FulfillmentConfigAccount; } public async getPhoenixV1FulfillmentConfig( phoenixMarket: PublicKey ): Promise { console.log(`getPhoenixV1FulfillmentConfig 1 programId = ${this.program.programId.toString()} phoenixMarket = ${phoenixMarket.toString()}`); const address = await getPhoenixFulfillmentConfigPublicKey( this.program.programId, phoenixMarket ); console.log(`getPhoenixV1FulfillmentConfig 2 address = ${address}`); return (await this.program.account?.phoenixV1FulfillmentConfig?.fetch( address )) as PhoenixV1FulfillmentConfigAccount; } public async fetchMarketLookupTableAccount(): Promise { if (this.lookupTableAccount) return this.lookupTableAccount; if (!this.marketLookupTable) { console.log('Market lookup table address not set'); return; } const lookupTableAccount = ( await this.connection.getAddressLookupTable(this.marketLookupTable) ).value; this.lookupTableAccount = lookupTableAccount; return lookupTableAccount; } /** * Update the wallet to use for drift transactions and linked user account * @param newWallet * @param subAccountIds * @param activeSubAccountId * @param includeDelegates */ public async updateWallet( newWallet: IWallet, subAccountIds?: number[], activeSubAccountId?: number, includeDelegates?: boolean, authoritySubaccountMap?: Map ): Promise { const newProvider = new AnchorProvider( this.connection, // @ts-ignore newWallet, this.opts ); const newProgram = new Program( driftIDL as Idl, this.program.programId, newProvider ); this.skipLoadUsers = false; // Update provider for txSender with new wallet details this.txSender.wallet = newWallet; this.wallet = newWallet; this.provider = newProvider; this.program = newProgram; this.authority = newWallet.publicKey; this.activeSubAccountId = activeSubAccountId; this.userStatsAccountPublicKey = undefined; this.includeDelegates = includeDelegates ?? false; const walletSupportsVersionedTxns = //@ts-ignore this.wallet.supportedTransactionVersions?.size ?? 0 > 1; this.txVersion = walletSupportsVersionedTxns ? 0 : 'legacy'; if (includeDelegates && subAccountIds) { throw new Error( 'Can only pass one of includeDelegates or subAccountIds. If you want to specify subaccount ids for multiple authorities, pass authoritySubaccountMap instead' ); } if (authoritySubaccountMap && subAccountIds) { throw new Error( 'Can only pass one of authoritySubaccountMap or subAccountIds' ); } if (authoritySubaccountMap && includeDelegates) { throw new Error( 'Can only pass one of authoritySubaccountMap or includeDelegates' ); } this.authoritySubAccountMap = authoritySubaccountMap ? authoritySubaccountMap : subAccountIds ? new Map([[this.authority.toString(), subAccountIds]]) : new Map(); let success = true; if (this.isSubscribed) { await Promise.all(this.unsubscribeUsers()); if (this.userStats) { await this.userStats.unsubscribe(); this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.getUserStatsAccountPublicKey(), accountSubscription: this.userStatsAccountSubscriptionConfig, }); await this.userStats.subscribe(); } this.users.clear(); success = await this.addAndSubscribeToUsers(); } return success; } public async switchActiveUser(subAccountId: number, authority?: PublicKey) { const authorityChanged = authority && !this.authority?.equals(authority); this.activeSubAccountId = subAccountId; this.authority = authority ?? this.authority; this.userStatsAccountPublicKey = getUserStatsAccountPublicKey( this.program.programId, this.authority ); /* If changing the user authority ie switching from delegate to non-delegate account, need to re-subscribe to the user stats account */ if (authorityChanged) { if (this.userStats && this.userStats.isSubscribed) { await this.userStats.unsubscribe(); } this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.userStatsAccountPublicKey, accountSubscription: this.userAccountSubscriptionConfig, }); this.userStats.subscribe(); } } public async addUser( subAccountId: number, authority?: PublicKey, userAccount?: UserAccount ): Promise { authority = authority ?? this.authority; const userKey = this.getUserMapKey(subAccountId, authority); if (this.users.has(userKey) && this.users.get(userKey).isSubscribed) { return true; } const user = this.createUser( subAccountId, this.userAccountSubscriptionConfig, authority ); const result = await user.subscribe(userAccount); if (result) { this.users.set(userKey, user); return true; } else { return false; } } /** * Adds and subscribes to users based on params set by the constructor or by updateWallet. */ public async addAndSubscribeToUsers(): Promise { // save the rpc calls if driftclient is initialized without a real wallet if (this.skipLoadUsers) return true; let result = true; if (this.authoritySubAccountMap && this.authoritySubAccountMap.size > 0) { this.authoritySubAccountMap.forEach(async (value, key) => { for (const subAccountId of value) { result = result && (await this.addUser(subAccountId, new PublicKey(key))); } }); if (this.activeSubAccountId == undefined) { this.switchActiveUser( [...this.authoritySubAccountMap.values()][0][0] ?? 0, new PublicKey( [...this.authoritySubAccountMap.keys()][0] ?? this.authority.toString() ) ); } } else { const userAccounts = (await this.getUserAccountsForAuthority(this.wallet.publicKey)) ?? []; let delegatedAccounts = []; if (this.includeDelegates) { delegatedAccounts = (await this.getUserAccountsForDelegate(this.wallet.publicKey)) ?? []; } for (const account of userAccounts.concat(delegatedAccounts)) { result = result && (await this.addUser( account.subAccountId, account.authority, account )); } if (this.activeSubAccountId == undefined) { this.switchActiveUser( userAccounts.concat(delegatedAccounts)[0]?.subAccountId ?? 0, userAccounts.concat(delegatedAccounts)[0]?.authority ?? this.authority ); } } return result; } public async initializeUserAccount( subAccountId = 0, name?: string, referrerInfo?: ReferrerInfo ): Promise<[TransactionSignature, PublicKey]> { const initializeIxs = []; const [userAccountPublicKey, initializeUserAccountIx] = await this.getInitializeUserInstructions( subAccountId, name, referrerInfo ); if (subAccountId === 0) { if ( !(await this.checkIfAccountExists(this.getUserStatsAccountPublicKey())) ) { initializeIxs.push(await this.getInitializeUserStatsIx()); } } initializeIxs.push(initializeUserAccountIx); const tx = await this.buildTransaction(initializeIxs); const { txSig } = await this.sendTransaction(tx, [], this.opts); await this.addUser(subAccountId); return [txSig, userAccountPublicKey]; } async getInitializeUserInstructions( subAccountId = 0, name?: string, referrerInfo?: ReferrerInfo ): Promise<[PublicKey, TransactionInstruction]> { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, this.wallet.publicKey, subAccountId ); const remainingAccounts = new Array(); if (referrerInfo !== undefined) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } const state = this.getStateAccount(); if (!state.whitelistMint.equals(PublicKey.default)) { const associatedTokenPublicKey = await getAssociatedTokenAddress( state.whitelistMint, this.wallet.publicKey ); remainingAccounts.push({ pubkey: associatedTokenPublicKey, isWritable: false, isSigner: false, }); } if (name === undefined) { if (subAccountId === 0) { name = DEFAULT_USER_NAME; } else { name = `Subaccount ${subAccountId + 1}`; } } const nameBuffer = encodeName(name); const initializeUserAccountIx = await this.program.instruction.initializeUser(subAccountId, nameBuffer, { accounts: { user: userAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, payer: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, state: await this.getStatePublicKey(), }, remainingAccounts, }); return [userAccountPublicKey, initializeUserAccountIx]; } async getInitializeUserStatsIx(): Promise { return await this.program.instruction.initializeUserStats({ accounts: { userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, payer: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, state: await this.getStatePublicKey(), }, }); } async getNextSubAccountId(): Promise { const userStats = this.getUserStats(); let userStatsAccount: UserStatsAccount; if (!userStats) { userStatsAccount = await fetchUserStatsAccount( this.connection, this.program, this.wallet.publicKey ); } else { userStatsAccount = userStats.getAccount(); } return userStatsAccount.numberOfSubAccountsCreated; } public async initializeReferrerName( name: string ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, 0 ); const nameBuffer = encodeName(name); const referrerNameAccountPublicKey = getReferrerNamePublicKeySync( this.program.programId, nameBuffer ); const tx = await this.program.transaction.initializeReferrerName( nameBuffer, { accounts: { referrerName: referrerNameAccountPublicKey, user: userAccountPublicKey, authority: this.wallet.publicKey, userStats: this.getUserStatsAccountPublicKey(), payer: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, } ); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async updateUserName( name: string, subAccountId = 0 ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ); const nameBuffer = encodeName(name); const tx = await this.program.transaction.updateUserName( subAccountId, nameBuffer, { accounts: { user: userAccountPublicKey, authority: this.wallet.publicKey, }, } ); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async updateUserCustomMarginRatio( updates: { marginRatio: number; subAccountId: number }[], txParams?: TxParams ): Promise { const ixs = await Promise.all( updates.map(async ({ marginRatio, subAccountId }) => { const ix = await this.getUpdateUserCustomMarginRatioIx( marginRatio, subAccountId ); return ix; }) ); const tx = await this.buildTransaction(ixs, txParams ?? this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getUpdateUserCustomMarginRatioIx( marginRatio: number, subAccountId = 0 ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ); await this.addUser(subAccountId, this.wallet.publicKey); const ix = this.program.instruction.updateUserCustomMarginRatio( subAccountId, marginRatio, { accounts: { user: userAccountPublicKey, authority: this.wallet.publicKey, }, } ); return ix; } public async getUpdateUserMarginTradingEnabledIx( marginTradingEnabled: boolean, subAccountId = 0, userAccountPublicKey?: PublicKey ): Promise { const userAccountPublicKeyToUse = userAccountPublicKey || getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ); await this.addUser(subAccountId, this.wallet.publicKey); let remainingAccounts; try { remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], }); } catch (err) { remainingAccounts = []; } return await this.program.instruction.updateUserMarginTradingEnabled( subAccountId, marginTradingEnabled, { accounts: { user: userAccountPublicKeyToUse, authority: this.wallet.publicKey, }, remainingAccounts, } ); } public async updateUserMarginTradingEnabled( updates: { marginTradingEnabled: boolean; subAccountId: number }[] ): Promise { const ixs = await Promise.all( updates.map(async ({ marginTradingEnabled, subAccountId }) => { return await this.getUpdateUserMarginTradingEnabledIx( marginTradingEnabled, subAccountId ); }) ); const tx = await this.buildTransaction(ixs, this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async updateUserDelegate( delegate: PublicKey, subAccountId = 0 ): Promise { const tx = await this.program.transaction.updateUserDelegate( subAccountId, delegate, { accounts: { user: await this.getUserAccountPublicKey(), authority: this.wallet.publicKey, }, } ); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async fetchAllUserAccounts( includeIdle = true ): Promise[]> { let filters = undefined; if (!includeIdle) { filters = [getNonIdleUserFilter()]; } return (await this.program.account.user.all( filters )) as ProgramAccount[]; } public async getUserAccountsForDelegate( delegate: PublicKey ): Promise { const programAccounts = await this.program.account.user.all([ { memcmp: { offset: 40, /** data to match, as base-58 encoded string and limited to less than 129 bytes */ bytes: bs58.encode(delegate.toBuffer()), }, }, ]); return programAccounts .map((programAccount) => programAccount.account as UserAccount) .sort((a, b) => a.subAccountId - b.subAccountId); } public async getUserAccountsAndAddressesForAuthority( authority: PublicKey ): Promise[]> { const programAccounts = await this.program.account.user.all([ { memcmp: { offset: 8, /** data to match, as base-58 encoded string and limited to less than 129 bytes */ bytes: bs58.encode(authority.toBuffer()), }, }, ]); return programAccounts.map( (programAccount) => programAccount as ProgramAccount ); } public async getUserAccountsForAuthority( authority: PublicKey ): Promise { const programAccounts = await this.program.account.user.all([ { memcmp: { offset: 8, /** data to match, as base-58 encoded string and limited to less than 129 bytes */ bytes: bs58.encode(authority.toBuffer()), }, }, ]); return programAccounts .map((programAccount) => programAccount.account as UserAccount) .sort((a, b) => a.subAccountId - b.subAccountId); } public async getReferredUserStatsAccountsByReferrer( referrer: PublicKey ): Promise { const programAccounts = await this.program.account.userStats.all([ { memcmp: { offset: 40, /** data to match, as base-58 encoded string and limited to less than 129 bytes */ bytes: bs58.encode(referrer.toBuffer()), }, }, ]); return programAccounts.map( (programAccount) => programAccount.account as UserStatsAccount ); } public async getReferrerNameAccountsForAuthority( authority: PublicKey ): Promise { const programAccounts = await this.program.account.referrerName.all([ { memcmp: { offset: 8, /** data to match, as base-58 encoded string and limited to less than 129 bytes */ bytes: bs58.encode(authority.toBuffer()), }, }, ]); return programAccounts.map( (programAccount) => programAccount.account as ReferrerNameAccount ); } public async deleteUser( subAccountId = 0, txParams?: TxParams ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ); const ix = await this.getUserDeletionIx(userAccountPublicKey); const { txSig } = await this.sendTransaction( await this.buildTransaction(ix, txParams), [], this.opts ); const userMapKey = this.getUserMapKey(subAccountId, this.wallet.publicKey); await this.users.get(userMapKey)?.unsubscribe(); this.users.delete(userMapKey); return txSig; } public async getUserDeletionIx(userAccountPublicKey: PublicKey) { const ix = await this.program.instruction.deleteUser({ accounts: { user: userAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, state: await this.getStatePublicKey(), }, }); return ix; } public async reclaimRent( subAccountId = 0, txParams?: TxParams ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ); const ix = await this.getReclaimRentIx(userAccountPublicKey); const { txSig } = await this.sendTransaction( await this.buildTransaction(ix, txParams), [], this.opts ); return txSig; } public async getReclaimRentIx(userAccountPublicKey: PublicKey) { return await this.program.instruction.reclaimRent({ accounts: { user: userAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, state: await this.getStatePublicKey(), rent: anchor.web3.SYSVAR_RENT_PUBKEY, }, }); } public getUser(subAccountId?: number, authority?: PublicKey): User { subAccountId = subAccountId ?? this.activeSubAccountId; authority = authority ?? this.authority; const userMapKey = this.getUserMapKey(subAccountId, authority); if (!this.users.has(userMapKey)) { throw new Error(`DriftClient has no user for user id ${userMapKey}`); } return this.users.get(userMapKey); } public hasUser(subAccountId?: number, authority?: PublicKey): boolean { subAccountId = subAccountId ?? this.activeSubAccountId; authority = authority ?? this.authority; const userMapKey = this.getUserMapKey(subAccountId, authority); return this.users.has(userMapKey); } public getUsers(): User[] { // delegate users get added to the end return [...this.users.values()] .filter((acct) => acct.getUserAccount().authority.equals(this.wallet.publicKey) ) .concat( [...this.users.values()].filter( (acct) => !acct.getUserAccount().authority.equals(this.wallet.publicKey) ) ); } public getUserStats(): UserStats { return this.userStats; } public async fetchReferrerNameAccount( name: string ): Promise { const nameBuffer = encodeName(name); const referrerNameAccountPublicKey = getReferrerNamePublicKeySync( this.program.programId, nameBuffer ); return (await this.program.account.referrerName.fetch( referrerNameAccountPublicKey )) as ReferrerNameAccount; } userStatsAccountPublicKey: PublicKey; public getUserStatsAccountPublicKey(): PublicKey { if (this.userStatsAccountPublicKey) { return this.userStatsAccountPublicKey; } this.userStatsAccountPublicKey = getUserStatsAccountPublicKey( this.program.programId, this.authority ); return this.userStatsAccountPublicKey; } public async getUserAccountPublicKey( subAccountId?: number, authority?: PublicKey ): Promise { return this.getUser(subAccountId, authority).userAccountPublicKey; } public getUserAccount( subAccountId?: number, authority?: PublicKey ): UserAccount | undefined { return this.getUser(subAccountId, authority).getUserAccount(); } /** * Forces a fetch to rpc before returning accounts. Useful for anchor tests. * @param subAccountId */ public async forceGetUserAccount( subAccountId?: number ): Promise { await this.getUser(subAccountId).fetchAccounts(); return this.getUser(subAccountId).getUserAccount(); } public getUserAccountAndSlot( subAccountId?: number ): DataAndSlot | undefined { return this.getUser(subAccountId).getUserAccountAndSlot(); } public getSpotPosition( marketIndex: number, subAccountId?: number ): SpotPosition | undefined { return this.getUserAccount(subAccountId).spotPositions.find( (spotPosition) => spotPosition.marketIndex === marketIndex ); } public getQuoteAssetTokenAmount(): BN { return this.getTokenAmount(QUOTE_SPOT_MARKET_INDEX); } /** * Returns the token amount for a given market. The spot market precision is based on the token mint decimals. * Positive if it is a deposit, negative if it is a borrow. * @param marketIndex */ public getTokenAmount(marketIndex: number): BN { const spotPosition = this.getSpotPosition(marketIndex); if (spotPosition === undefined) { return ZERO; } const spotMarket = this.getSpotMarketAccount(marketIndex); return getSignedTokenAmount( getTokenAmount( spotPosition.scaledBalance, spotMarket, spotPosition.balanceType ), spotPosition.balanceType ); } /** * Converts an amount to the spot precision for a given market. The spot market precision is based on the token mint decimals. * @param marketIndex * @param amount */ public convertToSpotPrecision(marketIndex: number, amount: BN | number): BN { const spotMarket = this.getSpotMarketAccount(marketIndex); return castNumberToSpotPrecision(amount, spotMarket); } /** * Converts an amount to the perp precision. The perp market precision is {@link BASE_PRECISION} (1e9). * @param amount */ public convertToPerpPrecision(amount: BN | number): BN { amount = typeof amount === 'number' ? new BN(amount) : amount; return amount.mul(BASE_PRECISION); } /** * Converts an amount to the price precision. The perp market precision is {@link PRICE_PRECISION} (1e6). * @param amount */ public convertToPricePrecision(amount: BN | number): BN { amount = typeof amount === 'number' ? new BN(amount) : amount; return amount.mul(PRICE_PRECISION); } /** * Each drift instruction must include perp and sport market accounts in the ix remaining accounts. * Use this function to force a subset of markets to be included in the remaining accounts for every ix * * @param perpMarketIndexes * @param spotMarketIndexes */ public mustIncludeMarketsInIx({ perpMarketIndexes, spotMarketIndexes, }: { perpMarketIndexes: number[]; spotMarketIndexes: number[]; }): void { perpMarketIndexes.forEach((perpMarketIndex) => { this.mustIncludePerpMarketIndexes.add(perpMarketIndex); }); spotMarketIndexes.forEach((spotMarketIndex) => { this.mustIncludeSpotMarketIndexes.add(spotMarketIndex); }); } getRemainingAccounts(params: RemainingAccountParams): AccountMeta[] { const { oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap } = this.getRemainingAccountMapsForUsers(params.userAccounts); if (params.useMarketLastSlotCache) { const lastUserSlot = this.getUserAccountAndSlot()?.slot; for (const [ marketIndex, slot, ] of this.perpMarketLastSlotCache.entries()) { // if cache has more recent slot than user positions account slot, add market to remaining accounts // otherwise remove from slot if (slot > lastUserSlot) { this.addPerpMarketToRemainingAccountMaps( marketIndex, false, oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap ); } else { this.perpMarketLastSlotCache.delete(marketIndex); } } for (const [ marketIndex, slot, ] of this.spotMarketLastSlotCache.entries()) { // if cache has more recent slot than user positions account slot, add market to remaining accounts // otherwise remove from slot if (slot > lastUserSlot) { this.addSpotMarketToRemainingAccountMaps( marketIndex, false, oracleAccountMap, spotMarketAccountMap ); } else { this.spotMarketLastSlotCache.delete(marketIndex); } } } if (params.readablePerpMarketIndex !== undefined) { const readablePerpMarketIndexes = Array.isArray( params.readablePerpMarketIndex ) ? params.readablePerpMarketIndex : [params.readablePerpMarketIndex]; for (const marketIndex of readablePerpMarketIndexes) { this.addPerpMarketToRemainingAccountMaps( marketIndex, false, oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap ); } } for (const perpMarketIndex of this.mustIncludePerpMarketIndexes.values()) { this.addPerpMarketToRemainingAccountMaps( perpMarketIndex, false, oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap ); } if (params.readableSpotMarketIndexes !== undefined) { for (const readableSpotMarketIndex of params.readableSpotMarketIndexes) { this.addSpotMarketToRemainingAccountMaps( readableSpotMarketIndex, false, oracleAccountMap, spotMarketAccountMap ); } } for (const spotMarketIndex of this.mustIncludeSpotMarketIndexes.values()) { this.addSpotMarketToRemainingAccountMaps( spotMarketIndex, false, oracleAccountMap, spotMarketAccountMap ); } if (params.writablePerpMarketIndexes !== undefined) { for (const writablePerpMarketIndex of params.writablePerpMarketIndexes) { this.addPerpMarketToRemainingAccountMaps( writablePerpMarketIndex, true, oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap ); } } if (params.writableSpotMarketIndexes !== undefined) { for (const writableSpotMarketIndex of params.writableSpotMarketIndexes) { this.addSpotMarketToRemainingAccountMaps( writableSpotMarketIndex, true, oracleAccountMap, spotMarketAccountMap ); } } return [ ...oracleAccountMap.values(), ...spotMarketAccountMap.values(), ...perpMarketAccountMap.values(), ]; } addPerpMarketToRemainingAccountMaps( marketIndex: number, writable: boolean, oracleAccountMap: Map, spotMarketAccountMap: Map, perpMarketAccountMap: Map ): void { const perpMarketAccount = this.getPerpMarketAccount(marketIndex); perpMarketAccountMap.set(marketIndex, { pubkey: perpMarketAccount.pubkey, isSigner: false, isWritable: writable, }); oracleAccountMap.set(perpMarketAccount.amm.oracle.toString(), { pubkey: perpMarketAccount.amm.oracle, isSigner: false, isWritable: false, }); this.addSpotMarketToRemainingAccountMaps( perpMarketAccount.quoteSpotMarketIndex, false, oracleAccountMap, spotMarketAccountMap ); } addSpotMarketToRemainingAccountMaps( marketIndex: number, writable: boolean, oracleAccountMap: Map, spotMarketAccountMap: Map ): void { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); spotMarketAccountMap.set(spotMarketAccount.marketIndex, { pubkey: spotMarketAccount.pubkey, isSigner: false, isWritable: writable, }); if (!spotMarketAccount.oracle.equals(PublicKey.default)) { oracleAccountMap.set(spotMarketAccount.oracle.toString(), { pubkey: spotMarketAccount.oracle, isSigner: false, isWritable: false, }); } } getRemainingAccountMapsForUsers(userAccounts: UserAccount[]): { oracleAccountMap: Map; spotMarketAccountMap: Map; perpMarketAccountMap: Map; } { const oracleAccountMap = new Map(); const spotMarketAccountMap = new Map(); const perpMarketAccountMap = new Map(); for (const userAccount of userAccounts) { for (const spotPosition of userAccount.spotPositions) { if (!isSpotPositionAvailable(spotPosition)) { this.addSpotMarketToRemainingAccountMaps( spotPosition.marketIndex, false, oracleAccountMap, spotMarketAccountMap ); if ( !spotPosition.openAsks.eq(ZERO) || !spotPosition.openBids.eq(ZERO) ) { this.addSpotMarketToRemainingAccountMaps( QUOTE_SPOT_MARKET_INDEX, false, oracleAccountMap, spotMarketAccountMap ); } } } for (const position of userAccount.perpPositions) { if (!positionIsAvailable(position)) { this.addPerpMarketToRemainingAccountMaps( position.marketIndex, false, oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap ); } } } return { oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap, }; } public getOrder(orderId: number, subAccountId?: number): Order | undefined { return this.getUserAccount(subAccountId)?.orders.find( (order) => order.orderId === orderId ); } public getOrderByUserId( userOrderId: number, subAccountId?: number ): Order | undefined { return this.getUserAccount(subAccountId)?.orders.find( (order) => order.userOrderId === userOrderId ); } /** * Get the associated token address for the given spot market * @param marketIndex * @param useNative */ public async getAssociatedTokenAccount( marketIndex: number, useNative = true ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); if (useNative && spotMarket.mint.equals(WRAPPED_SOL_MINT)) { return this.wallet.publicKey; } const mint = spotMarket.mint; return await getAssociatedTokenAddress(mint, this.wallet.publicKey); } public createAssociatedTokenAccountIdempotentInstruction( account: PublicKey, payer: PublicKey, owner: PublicKey, mint: PublicKey ): TransactionInstruction { return new TransactionInstruction({ keys: [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: account, isSigner: false, isWritable: true }, { pubkey: owner, isSigner: false, isWritable: false }, { pubkey: mint, isSigner: false, isWritable: false }, { pubkey: anchor.web3.SystemProgram.programId, isSigner: false, isWritable: false, }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, ], programId: ASSOCIATED_TOKEN_PROGRAM_ID, data: Buffer.from([0x1]), }); } /** * Deposit funds into the given spot market * * @param amount to deposit * @param marketIndex spot market index to deposit into * @param associatedTokenAccount can be the wallet public key if using native sol * @param subAccountId subaccountId to deposit * @param reduceOnly if true, deposit must not increase account risk */ public async deposit( amount: BN, marketIndex: number, associatedTokenAccount: PublicKey, subAccountId?: number, reduceOnly = false, txParams?: TxParams ): Promise { const additionalSigners: Array = []; const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const isSolMarket = spotMarketAccount.mint.equals(WRAPPED_SOL_MINT); const signerAuthority = this.wallet.publicKey; const createWSOLTokenAccount = isSolMarket && associatedTokenAccount.equals(signerAuthority); const instructions = []; if (createWSOLTokenAccount) { const { ixs, pubkey } = await this.getWrappedSolAccountCreationIxs( amount, true ); associatedTokenAccount = pubkey; instructions.push(...ixs); } const depositCollateralIx = await this.getDepositInstruction( amount, marketIndex, associatedTokenAccount, subAccountId, reduceOnly, true ); instructions.push(depositCollateralIx); // Close the wrapped sol account at the end of the transaction if (createWSOLTokenAccount) { instructions.push( createCloseAccountInstruction( associatedTokenAccount, signerAuthority, signerAuthority, [] ) ); } txParams = { ...(txParams ?? this.txParams), computeUnits: 600_000 }; const tx = await this.buildTransaction(instructions, txParams); const { txSig, slot } = await this.sendTransaction( tx, additionalSigners, this.opts ); this.spotMarketLastSlotCache.set(marketIndex, slot); return txSig; } async getDepositInstruction( amount: BN, marketIndex: number, userTokenAccount: PublicKey, subAccountId?: number, reduceOnly = false, userInitialized = true ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, this.authority, subAccountId ?? this.activeSubAccountId ); let remainingAccounts = []; if (userInitialized) { remainingAccounts = this.getRemainingAccounts({ userAccounts: [await this.forceGetUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); } else { remainingAccounts = this.getRemainingAccounts({ userAccounts: [], writableSpotMarketIndexes: [marketIndex], }); } const spotMarketAccount = this.getSpotMarketAccount(marketIndex); return await this.program.instruction.deposit( marketIndex, amount, reduceOnly, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarketAccount.pubkey, spotMarketVault: spotMarketAccount.vault, user: userAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), userTokenAccount: userTokenAccount, authority: this.wallet.publicKey, tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts, } ); } private async checkIfAccountExists(account: PublicKey): Promise { try { const accountInfo = await this.connection.getAccountInfo(account); return accountInfo != null; } catch (e) { // Doesn't already exist return false; } } public async getWrappedSolAccountCreationIxs( amount: BN, includeRent?: boolean ): Promise<{ ixs: anchor.web3.TransactionInstruction[]; /** @deprecated - this array is always going to be empty, in the current implementation */ signers: Signer[]; pubkey: PublicKey; }> { const authority = this.wallet.publicKey; // Generate a random seed for wrappedSolAccount. const seed = Keypair.generate().publicKey.toBase58().slice(0, 32); // Calculate a publicKey that will be controlled by the authority. const wrappedSolAccount = await PublicKey.createWithSeed( authority, seed, TOKEN_PROGRAM_ID ); const result = { ixs: [], signers: [], pubkey: wrappedSolAccount, }; const rentSpaceLamports = new BN(LAMPORTS_PER_SOL / 100); const lamports = includeRent ? amount.add(rentSpaceLamports) : rentSpaceLamports; result.ixs.push( SystemProgram.createAccountWithSeed({ fromPubkey: authority, basePubkey: authority, seed, newAccountPubkey: wrappedSolAccount, lamports: lamports.toNumber(), space: 165, programId: TOKEN_PROGRAM_ID, }) ); result.ixs.push( createInitializeAccountInstruction( wrappedSolAccount, WRAPPED_SOL_MINT, authority ) ); return result; } public getAssociatedTokenAccountCreationIx( tokenMintAddress: PublicKey, associatedTokenAddress: PublicKey ): anchor.web3.TransactionInstruction { return createAssociatedTokenAccountInstruction( this.wallet.publicKey, associatedTokenAddress, this.wallet.publicKey, tokenMintAddress ); } /** * Creates the User account for a user, and deposits some initial collateral * @param amount * @param userTokenAccount * @param marketIndex * @param subAccountId * @param name * @param fromSubAccountId * @param referrerInfo * @param donateAmount * @param txParams * @returns */ public async initializeUserAccountAndDepositCollateral( amount: BN, userTokenAccount: PublicKey, marketIndex = 0, subAccountId = 0, name?: string, fromSubAccountId?: number, referrerInfo?: ReferrerInfo, donateAmount?: BN, txParams?: TxParams, customMaxMarginRatio?: number ): Promise<[TransactionSignature, PublicKey]> { const ixs = []; const [userAccountPublicKey, initializeUserAccountIx] = await this.getInitializeUserInstructions( subAccountId, name, referrerInfo ); const additionalSigners: Array = []; const spotMarket = this.getSpotMarketAccount(marketIndex); const isSolMarket = spotMarket.mint.equals(WRAPPED_SOL_MINT); let params: TxParams = { computeUnits: txParams?.computeUnits ?? 600_000, }; if (txParams?.computeUnitsPrice) { params = { ...params, computeUnitsPrice: txParams.computeUnitsPrice, }; } const authority = this.wallet.publicKey; const isFromSubaccount = fromSubAccountId !== null && fromSubAccountId !== undefined && !isNaN(fromSubAccountId); donateAmount = donateAmount ? donateAmount : ZERO; const createWSOLTokenAccount = (isSolMarket && userTokenAccount.equals(authority) && !isFromSubaccount) || !donateAmount.eq(ZERO); const wSolAmount = isSolMarket ? amount.add(donateAmount) : donateAmount; let wsolTokenAccount: PublicKey; if (createWSOLTokenAccount) { const { ixs: startIxs, pubkey } = await this.getWrappedSolAccountCreationIxs(wSolAmount, true); wsolTokenAccount = pubkey; if (isSolMarket) { userTokenAccount = pubkey; } ixs.push(...startIxs); } const depositCollateralIx = isFromSubaccount ? await this.getTransferDepositIx( amount, marketIndex, fromSubAccountId, subAccountId ) : await this.getDepositInstruction( amount, marketIndex, userTokenAccount, subAccountId, false, false ); if (subAccountId === 0) { if ( !(await this.checkIfAccountExists(this.getUserStatsAccountPublicKey())) ) { ixs.push(await this.getInitializeUserStatsIx()); } } ixs.push(initializeUserAccountIx, depositCollateralIx); if (!donateAmount.eq(ZERO)) { const donateIx = await this.getDepositIntoSpotMarketRevenuePoolIx( 1, donateAmount, wsolTokenAccount ); ixs.push(donateIx); } // Set the max margin ratio to initialize account with if passed if (customMaxMarginRatio) { const customMarginRatioIx = await this.getUpdateUserCustomMarginRatioIx( customMaxMarginRatio, subAccountId ); ixs.push(customMarginRatioIx); } // Close the wrapped sol account at the end of the transaction if (createWSOLTokenAccount) { ixs.push( createCloseAccountInstruction( wsolTokenAccount, authority, authority, [] ) ); } const tx = await this.buildTransaction(ixs, params); const { txSig, slot } = await this.sendTransaction( tx, additionalSigners, this.opts ); this.spotMarketLastSlotCache.set(marketIndex, slot); await this.addUser(subAccountId); return [txSig, userAccountPublicKey]; } public async initializeUserAccountForDevnet( subAccountId = 0, name = DEFAULT_USER_NAME, marketIndex: number, tokenFaucet: TokenFaucet, amount: BN, referrerInfo?: ReferrerInfo ): Promise<[TransactionSignature, PublicKey]> { const ixs = []; const [associateTokenPublicKey, createAssociatedAccountIx, mintToIx] = await tokenFaucet.createAssociatedTokenAccountAndMintToInstructions( this.wallet.publicKey, amount ); const [userAccountPublicKey, initializeUserAccountIx] = await this.getInitializeUserInstructions( subAccountId, name, referrerInfo ); const depositCollateralIx = await this.getDepositInstruction( amount, marketIndex, associateTokenPublicKey, subAccountId, false, false ); ixs.push(createAssociatedAccountIx, mintToIx); if (subAccountId === 0) { if ( !(await this.checkIfAccountExists(this.getUserStatsAccountPublicKey())) ) { ixs.push(await this.getInitializeUserStatsIx()); } } ixs.push(initializeUserAccountIx, depositCollateralIx); const tx = await this.buildTransaction(ixs); const { txSig } = await this.sendTransaction(tx, [], this.opts); await this.addUser(subAccountId); return [txSig, userAccountPublicKey]; } /** * Withdraws from a user account. If deposit doesn't already exist, creates a borrow * @param amount * @param marketIndex * @param associatedTokenAddress - the token account to withdraw to. can be the wallet public key if using native sol * @param reduceOnly */ public async withdraw( amount: BN, marketIndex: number, associatedTokenAddress: PublicKey, reduceOnly = false, subAccountId?: number, txParams?: TxParams ): Promise { const withdrawIxs = []; const additionalSigners: Array = []; const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const isSolMarket = spotMarketAccount.mint.equals(WRAPPED_SOL_MINT); const authority = this.wallet.publicKey; const createWSOLTokenAccount = isSolMarket && associatedTokenAddress.equals(authority); if (createWSOLTokenAccount) { const { ixs, pubkey } = await this.getWrappedSolAccountCreationIxs( amount, false ); associatedTokenAddress = pubkey; withdrawIxs.push(...ixs); } else { const accountExists = await this.checkIfAccountExists( associatedTokenAddress ); if (!accountExists) { const createAssociatedTokenAccountIx = this.getAssociatedTokenAccountCreationIx( spotMarketAccount.mint, associatedTokenAddress ); withdrawIxs.push(createAssociatedTokenAccountIx); } } const withdrawCollateralIx = await this.getWithdrawIx( amount, spotMarketAccount.marketIndex, associatedTokenAddress, reduceOnly, subAccountId ); withdrawIxs.push(withdrawCollateralIx); // Close the wrapped sol account at the end of the transaction if (createWSOLTokenAccount) { withdrawIxs.push( createCloseAccountInstruction( associatedTokenAddress, authority, authority, [] ) ); } const tx = await this.buildTransaction(withdrawIxs, { ...(txParams ?? this.txParams), computeUnits: 1_400_000, }); const { txSig, slot } = await this.sendTransaction( tx, additionalSigners, this.opts ); this.spotMarketLastSlotCache.set(marketIndex, slot); return txSig; } public async getWithdrawIx( amount: BN, marketIndex: number, userTokenAccount: PublicKey, reduceOnly = false, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); const spotMarketAccount = this.getSpotMarketAccount(marketIndex); return await this.program.instruction.withdraw( marketIndex, amount, reduceOnly, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarketAccount.pubkey, spotMarketVault: spotMarketAccount.vault, driftSigner: this.getSignerPublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), userTokenAccount: userTokenAccount, authority: this.wallet.publicKey, tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts, } ); } /** * Withdraws from the fromSubAccount and deposits into the toSubAccount * @param amount * @param marketIndex * @param fromSubAccountId * @param toSubAccountId * @param txParams */ public async transferDeposit( amount: BN, marketIndex: number, fromSubAccountId: number, toSubAccountId: number, txParams?: TxParams ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getTransferDepositIx( amount, marketIndex, fromSubAccountId, toSubAccountId ), txParams ), [], this.opts ); if ( fromSubAccountId === this.activeSubAccountId || toSubAccountId === this.activeSubAccountId ) { this.spotMarketLastSlotCache.set(marketIndex, slot); } return txSig; } public async getTransferDepositIx( amount: BN, marketIndex: number, fromSubAccountId: number, toSubAccountId: number ): Promise { const fromUser = await getUserAccountPublicKey( this.program.programId, this.wallet.publicKey, fromSubAccountId ); const toUser = await getUserAccountPublicKey( this.program.programId, this.wallet.publicKey, toSubAccountId ); let remainingAccounts; const userMapKey = this.getUserMapKey( fromSubAccountId, this.wallet.publicKey ); if (this.users.has(userMapKey)) { remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.users.get(userMapKey).getUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); } else { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.authority, fromSubAccountId ); const fromUserAccount = (await this.program.account.user.fetch( userAccountPublicKey )) as UserAccount; remainingAccounts = this.getRemainingAccounts({ userAccounts: [fromUserAccount], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); } return await this.program.instruction.transferDeposit(marketIndex, amount, { accounts: { authority: this.wallet.publicKey, fromUser, toUser, userStats: this.getUserStatsAccountPublicKey(), state: await this.getStatePublicKey(), spotMarketVault: this.getSpotMarketAccount(marketIndex).vault, }, remainingAccounts, }); } public async updateSpotMarketCumulativeInterest( marketIndex: number, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.updateSpotMarketCumulativeInterestIx(marketIndex), txParams ), [], this.opts ); return txSig; } public async updateSpotMarketCumulativeInterestIx( marketIndex: number ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); return await this.program.instruction.updateSpotMarketCumulativeInterest({ accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarket.pubkey, spotMarketVault: spotMarket.vault, oracle: spotMarket.oracle, }, }); } public async settleLP( settleeUserAccountPublicKey: PublicKey, marketIndex: number, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.settleLPIx(settleeUserAccountPublicKey, marketIndex), txParams ), [], this.opts ); return txSig; } public async settleLPIx( settleeUserAccountPublicKey: PublicKey, marketIndex: number ): Promise { const settleeUserAccount = (await this.program.account.user.fetch( settleeUserAccountPublicKey )) as UserAccount; const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], writablePerpMarketIndexes: [marketIndex], }); return this.program.instruction.settleLp(marketIndex, { accounts: { state: await this.getStatePublicKey(), user: settleeUserAccountPublicKey, }, remainingAccounts: remainingAccounts, }); } public async removePerpLpShares( marketIndex: number, sharesToBurn?: BN, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getRemovePerpLpSharesIx( marketIndex, sharesToBurn, subAccountId ), txParams ), [], this.opts ); return txSig; } public async removePerpLpSharesInExpiringMarket( marketIndex: number, userAccountPublicKey: PublicKey, sharesToBurn?: BN, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getRemovePerpLpSharesInExpiringMarket( marketIndex, userAccountPublicKey, sharesToBurn ), txParams ), [], this.opts ); return txSig; } public async getRemovePerpLpSharesInExpiringMarket( marketIndex: number, userAccountPublicKey: PublicKey, sharesToBurn?: BN ): Promise { const userAccount = (await this.program.account.user.fetch( userAccountPublicKey )) as UserAccount; const remainingAccounts = this.getRemainingAccounts({ userAccounts: [userAccount], useMarketLastSlotCache: true, writablePerpMarketIndexes: [marketIndex], }); if (sharesToBurn == undefined) { const perpPosition = userAccount.perpPositions.filter( (position) => position.marketIndex === marketIndex )[0]; sharesToBurn = perpPosition.lpShares; console.log('burning lp shares:', sharesToBurn.toString()); } return this.program.instruction.removePerpLpSharesInExpiringMarket( sharesToBurn, marketIndex, { accounts: { state: await this.getStatePublicKey(), user: userAccountPublicKey, }, remainingAccounts: remainingAccounts, } ); } public async getRemovePerpLpSharesIx( marketIndex: number, sharesToBurn?: BN, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, writablePerpMarketIndexes: [marketIndex], }); if (sharesToBurn == undefined) { const userAccount = this.getUserAccount(subAccountId); const perpPosition = userAccount.perpPositions.filter( (position) => position.marketIndex === marketIndex )[0]; sharesToBurn = perpPosition.lpShares; console.log('burning lp shares:', sharesToBurn.toString()); } return this.program.instruction.removePerpLpShares( sharesToBurn, marketIndex, { accounts: { state: await this.getStatePublicKey(), user, authority: this.wallet.publicKey, }, remainingAccounts: remainingAccounts, } ); } public async addPerpLpShares( amount: BN, marketIndex: number, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getAddPerpLpSharesIx(amount, marketIndex, subAccountId), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(marketIndex, slot); return txSig; } public async getAddPerpLpSharesIx( amount: BN, marketIndex: number, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, writablePerpMarketIndexes: [marketIndex], }); return this.program.instruction.addPerpLpShares(amount, marketIndex, { accounts: { state: await this.getStatePublicKey(), user, authority: this.wallet.publicKey, }, remainingAccounts: remainingAccounts, }); } public getQuoteValuePerLpShare(marketIndex: number): BN { const perpMarketAccount = this.getPerpMarketAccount(marketIndex); const openBids = BN.max( perpMarketAccount.amm.baseAssetReserve.sub( perpMarketAccount.amm.minBaseAssetReserve ), ZERO ); const openAsks = BN.max( perpMarketAccount.amm.maxBaseAssetReserve.sub( perpMarketAccount.amm.baseAssetReserve ), ZERO ); const oraclePriceData = this.getOracleDataForPerpMarket(marketIndex); const maxOpenBidsAsks = BN.max(openBids, openAsks); const quoteValuePerLpShare = maxOpenBidsAsks .mul(oraclePriceData.price) .mul(QUOTE_PRECISION) .div(PRICE_PRECISION) .div(perpMarketAccount.amm.sqrtK); return quoteValuePerLpShare; } /** * @deprecated use {@link placePerpOrder} or {@link placeAndTakePerpOrder} instead */ public async openPosition( direction: PositionDirection, amount: BN, marketIndex: number, limitPrice?: BN, subAccountId?: number ): Promise { return await this.placeAndTakePerpOrder( { orderType: OrderType.MARKET, marketIndex, direction, baseAssetAmount: amount, price: limitPrice, }, undefined, undefined, undefined, subAccountId ); } public async sendSignedTx(tx: Transaction): Promise { const { txSig } = await this.sendTransaction( tx, undefined, this.opts, true ); return txSig; } /** * Sends a market order and returns a signed tx which can fill the order against the vamm, which the caller can use to fill their own order if required. * @param orderParams * @param userAccountPublicKey * @param userAccount * @param makerInfo * @param txParams * @param bracketOrdersParams * @param cancelExistingOrders - Builds and returns an extra transaciton to cancel the existing orders in the same perp market. Intended use is to auto-cancel TP/SL orders when closing a position. Ignored if orderParams.marketType is not MarketType.PERP * @returns */ public async sendMarketOrderAndGetSignedFillTx( orderParams: OptionalOrderParams, userAccountPublicKey: PublicKey, userAccount: UserAccount, makerInfo?: MakerInfo | MakerInfo[], txParams?: TxParams, bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, settlePnl?: boolean ): Promise<{ txSig: TransactionSignature; signedFillTx?: Transaction; signedCancelExistingOrdersTx?: Transaction; signedSettlePnlTx?: Transaction; }> { const marketIndex = orderParams.marketIndex; const orderId = userAccount.nextOrderId; const ordersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); /* Cancel open orders in market if requested */ let cancelExistingOrdersTx; if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { const cancelOrdersIx = await this.getCancelOrdersIx( orderParams.marketType, orderParams.marketIndex, null, userAccount.subAccountId ); //@ts-ignore cancelExistingOrdersTx = await this.buildTransaction( [cancelOrdersIx], txParams, this.txVersion ); } /* Settle PnL after fill if requested */ let settlePnlTx; if (settlePnl && isVariant(orderParams.marketType, 'perp')) { const settlePnlIx = await this.settlePNLIx( userAccountPublicKey, userAccount, marketIndex ); //@ts-ignore settlePnlTx = await this.buildTransaction( [settlePnlIx], txParams, this.txVersion ); } // use versioned transactions if there is a lookup table account and wallet is compatible if (this.txVersion === 0) { const versionedMarketOrderTx = await this.buildTransaction( ordersIx, txParams, 0 ); const fillPerpOrderIx = await this.getFillPerpOrderIx( userAccountPublicKey, userAccount, { orderId, marketIndex, }, makerInfo, referrerInfo, userAccount.subAccountId ); const versionedFillTx = await this.buildTransaction( [fillPerpOrderIx], txParams, 0 ); const allPossibleTxs = [ versionedMarketOrderTx, versionedFillTx, cancelExistingOrdersTx, settlePnlTx, ]; const txKeys = [ 'signedVersionedMarketOrderTx', 'signedVersionedFillTx', 'signedCancelExistingOrdersTx', 'signedSettlePnlTx', ]; const { signedVersionedMarketOrderTx, signedVersionedFillTx, signedCancelExistingOrdersTx, signedSettlePnlTx, } = await getSignedTransactionMap( //@ts-ignore this.provider.wallet, allPossibleTxs, txKeys ); const { txSig, slot } = await this.sendTransaction( signedVersionedMarketOrderTx, [], this.opts, true ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return { txSig, // @ts-ignore signedFillTx: signedVersionedFillTx, // @ts-ignore signedCancelExistingOrdersTx, // @ts-ignore signedSettlePnlTx, }; } else { const marketOrderTx = wrapInTx( ordersIx, txParams?.computeUnits, txParams?.computeUnitsPrice ); // Apply the latest blockhash to the txs so that we can sign before sending them const currentBlockHash = ( await this.connection.getLatestBlockhash('finalized') ).blockhash; marketOrderTx.recentBlockhash = currentBlockHash; marketOrderTx.feePayer = userAccount.authority; if (cancelExistingOrdersTx) { cancelExistingOrdersTx.recentBlockhash = currentBlockHash; cancelExistingOrdersTx.feePayer = userAccount.authority; } if (settlePnlTx) { settlePnlTx.recentBlockhash = currentBlockHash; settlePnlTx.feePayer = userAccount.authority; } const allPossibleTxs = [ marketOrderTx, cancelExistingOrdersTx, settlePnlTx, ]; const txKeys = [ 'signedMarketOrderTx', 'signedCancelExistingOrdersTx', 'signedSettlePnlTx', ]; const { signedMarketOrderTx, signedCancelExistingOrdersTx, signedSettlePnlTx, } = await getSignedTransactionMap( //@ts-ignore this.provider.wallet, allPossibleTxs, txKeys ); const { txSig, slot } = await this.sendTransaction( signedMarketOrderTx, [], this.opts, true ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return { txSig, signedFillTx: undefined, //@ts-ignore signedCancelExistingOrdersTx, //@ts-ignore signedSettlePnlTx, }; } } public async placePerpOrder( orderParams: OptionalOrderParams, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlacePerpOrderIx(orderParams, subAccountId), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } public async getPlacePerpOrderIx( orderParams: OptionalOrderParams, subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.PERP }); const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, readablePerpMarketIndex: orderParams.marketIndex, }); return await this.program.instruction.placePerpOrder(orderParams, { accounts: { state: await this.getStatePublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, }); } public async updateAMMs( marketIndexes: number[], txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getUpdateAMMsIx(marketIndexes), txParams ), [], this.opts ); return txSig; } public async getUpdateAMMsIx( marketIndexes: number[] ): Promise { for (let i = marketIndexes.length; i < 5; i++) { marketIndexes.push(100); } const marketAccountInfos = []; const oracleAccountInfos = []; for (const marketIndex of marketIndexes) { if (marketIndex !== 100) { const market = this.getPerpMarketAccount(marketIndex); marketAccountInfos.push({ pubkey: market.pubkey, isWritable: true, isSigner: false, }); oracleAccountInfos.push({ pubkey: market.amm.oracle, isWritable: false, isSigner: false, }); } } const remainingAccounts = oracleAccountInfos.concat(marketAccountInfos); return await this.program.instruction.updateAmms(marketIndexes, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, }); } public async settleExpiredMarket( marketIndex: number, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getSettleExpiredMarketIx(marketIndex), txParams ), [], this.opts ); return txSig; } public async getSettleExpiredMarketIx( marketIndex: number ): Promise { const marketAccountInfos = []; const oracleAccountInfos = []; const spotMarketAccountInfos = []; const market = this.getPerpMarketAccount(marketIndex); marketAccountInfos.push({ pubkey: market.pubkey, isWritable: true, isSigner: false, }); oracleAccountInfos.push({ pubkey: market.amm.oracle, isWritable: false, isSigner: false, }); spotMarketAccountInfos.push({ pubkey: this.getSpotMarketAccount(QUOTE_SPOT_MARKET_INDEX).pubkey, isSigner: false, isWritable: true, }); const remainingAccounts = oracleAccountInfos .concat(spotMarketAccountInfos) .concat(marketAccountInfos); return await this.program.instruction.settleExpiredMarket(marketIndex, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, }); } public async settleExpiredMarketPoolsToRevenuePool( perpMarketIndex: number, txParams?: TxParams ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, perpMarketIndex ); const spotMarketPublicKey = await getSpotMarketPublicKey( this.program.programId, QUOTE_SPOT_MARKET_INDEX ); const ix = await this.program.instruction.settleExpiredMarketPoolsToRevenuePool({ accounts: { state: await this.getStatePublicKey(), admin: this.wallet.publicKey, spotMarket: spotMarketPublicKey, perpMarket: perpMarketPublicKey, }, }); const { txSig } = await this.sendTransaction( await this.buildTransaction(ix, txParams), [], this.opts ); return txSig; } public async cancelOrder( orderId?: number, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getCancelOrderIx(orderId, subAccountId), txParams ), [], this.opts ); return txSig; } public async getCancelOrderIx( orderId?: number, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, }); return await this.program.instruction.cancelOrder(orderId ?? null, { accounts: { state: await this.getStatePublicKey(), user, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async cancelOrderByUserId( userOrderId: number, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getCancelOrderByUserIdIx(userOrderId, subAccountId), txParams ), [], this.opts ); return txSig; } public async getCancelOrderByUserIdIx( userOrderId: number, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const order = this.getOrderByUserId(userOrderId); const oracle = this.getPerpMarketAccount(order.marketIndex).amm.oracle; const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, }); return await this.program.instruction.cancelOrderByUserId(userOrderId, { accounts: { state: await this.getStatePublicKey(), user, authority: this.wallet.publicKey, oracle, }, remainingAccounts, }); } public async cancelOrdersByIds( orderIds?: number[], txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getCancelOrdersByIdsIx(orderIds, subAccountId), txParams ), [], this.opts ); return txSig; } public async getCancelOrdersByIdsIx( orderIds?: number[], subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, }); return await this.program.instruction.cancelOrdersByIds(orderIds, { accounts: { state: await this.getStatePublicKey(), user, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async cancelOrders( marketType?: MarketType, marketIndex?: number, direction?: PositionDirection, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getCancelOrdersIx( marketType, marketIndex, direction, subAccountId ), txParams ), [], this.opts ); return txSig; } public async getCancelOrdersIx( marketType: MarketType | null, marketIndex: number | null, direction: PositionDirection | null, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); let readablePerpMarketIndex = undefined; let readableSpotMarketIndexes = undefined; if (typeof marketIndex === 'number') { if (marketType && isVariant(marketType, 'perp')) { readablePerpMarketIndex = marketIndex; } else if (marketType && isVariant(marketType, 'spot')) { readableSpotMarketIndexes = [marketIndex]; } } const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], readablePerpMarketIndex, readableSpotMarketIndexes, useMarketLastSlotCache: true, }); return await this.program.instruction.cancelOrders( marketType ?? null, marketIndex ?? null, direction ?? null, { accounts: { state: await this.getStatePublicKey(), user, authority: this.wallet.publicKey, }, remainingAccounts, } ); } public async cancelAndPlaceOrders( cancelOrderParams: { marketType?: MarketType; marketIndex?: number; direction?: PositionDirection; }, placeOrderParams: OrderParams[], txParams?: TxParams, subAccountId?: number ): Promise { const ixs = [ await this.getCancelOrdersIx( cancelOrderParams.marketType, cancelOrderParams.marketIndex, cancelOrderParams.direction, subAccountId ), await this.getPlaceOrdersIx(placeOrderParams, subAccountId), ]; const tx = await this.buildTransaction(ixs, txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async placeOrders( params: OrderParams[], txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getPlaceOrdersIx(params, subAccountId), txParams ), [], this.opts ); return txSig; } public async getPlaceOrdersIx( params: OptionalOrderParams[], subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const readablePerpMarketIndex: number[] = []; const readableSpotMarketIndexes: number[] = []; for (const param of params) { if (!param.marketType) { throw new Error('must set param.marketType'); } if (isVariant(param.marketType, 'perp')) { readablePerpMarketIndex.push(param.marketIndex); } else { readableSpotMarketIndexes.push(param.marketIndex); } } const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], readablePerpMarketIndex, readableSpotMarketIndexes, useMarketLastSlotCache: true, }); const formattedParams = params.map((item) => getOrderParams(item)); return await this.program.instruction.placeOrders(formattedParams, { accounts: { state: await this.getStatePublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, }); } public async fillPerpOrder( userAccountPublicKey: PublicKey, user: UserAccount, order?: Pick, makerInfo?: MakerInfo | MakerInfo[], referrerInfo?: ReferrerInfo, txParams?: TxParams, fillerPublicKey?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getFillPerpOrderIx( userAccountPublicKey, user, order, makerInfo, referrerInfo, fillerPublicKey ), txParams ), [], this.opts ); return txSig; } public async getFillPerpOrderIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, order: Pick, makerInfo?: MakerInfo | MakerInfo[], referrerInfo?: ReferrerInfo, fillerSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const filler = await this.getUserAccountPublicKey(fillerSubAccountId); const fillerStatsPublicKey = this.getUserStatsAccountPublicKey(); const marketIndex = order ? order.marketIndex : userAccount.orders.find( (order) => order.orderId === userAccount.nextOrderId - 1 ).marketIndex; makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo ? [makerInfo] : []; const userAccounts = [userAccount]; for (const maker of makerInfo) { userAccounts.push(maker.makerUserAccount); } const remainingAccounts = this.getRemainingAccounts({ userAccounts, writablePerpMarketIndexes: [marketIndex], }); for (const maker of makerInfo) { remainingAccounts.push({ pubkey: maker.maker, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: maker.makerStats, isWritable: true, isSigner: false, }); } if (referrerInfo) { const referrerIsMaker = makerInfo.find((maker) => maker.maker.equals(referrerInfo.referrer)) !== undefined; if (!referrerIsMaker) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } } const orderId = order.orderId; return await this.program.instruction.fillPerpOrder(orderId, null, { accounts: { state: await this.getStatePublicKey(), filler, fillerStats: fillerStatsPublicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async getRevertFillIx( fillerPublicKey?: PublicKey ): Promise { const filler = fillerPublicKey ?? (await this.getUserAccountPublicKey()); const fillerStatsPublicKey = this.getUserStatsAccountPublicKey(); return this.program.instruction.revertFill({ accounts: { state: await this.getStatePublicKey(), filler, fillerStats: fillerStatsPublicKey, authority: this.wallet.publicKey, }, }); } public async placeSpotOrder( orderParams: OptionalOrderParams, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlaceSpotOrderIx(orderParams, subAccountId), txParams ), [], this.opts ); this.spotMarketLastSlotCache.set(orderParams.marketIndex, slot); this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } public async getPlaceSpotOrderIx( orderParams: OptionalOrderParams, subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.SPOT }); const userAccountPublicKey = await this.getUserAccountPublicKey( subAccountId ); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, readableSpotMarketIndexes: [ orderParams.marketIndex, QUOTE_SPOT_MARKET_INDEX, ], }); return await this.program.instruction.placeSpotOrder(orderParams, { accounts: { state: await this.getStatePublicKey(), user: userAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, }); } public async fillSpotOrder( userAccountPublicKey: PublicKey, user: UserAccount, order?: Order, fulfillmentConfig?: | SerumV3FulfillmentConfigAccount | PhoenixV1FulfillmentConfigAccount, makerInfo?: MakerInfo, referrerInfo?: ReferrerInfo, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getFillSpotOrderIx( userAccountPublicKey, user, order, fulfillmentConfig, makerInfo, referrerInfo ), txParams ), [], this.opts ); return txSig; } public async getFillSpotOrderIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, order?: Order, fulfillmentConfig?: | SerumV3FulfillmentConfigAccount | PhoenixV1FulfillmentConfigAccount, makerInfo?: MakerInfo, referrerInfo?: ReferrerInfo, fillerPublicKey?: PublicKey ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const filler = fillerPublicKey ?? (await this.getUserAccountPublicKey()); const fillerStatsPublicKey = this.getUserStatsAccountPublicKey(); const marketIndex = order ? order.marketIndex : userAccount.orders.find( (order) => order.orderId === userAccount.nextOrderId - 1 ).marketIndex; const userAccounts = [userAccount]; if (makerInfo !== undefined) { userAccounts.push(makerInfo.makerUserAccount); } const remainingAccounts = this.getRemainingAccounts({ userAccounts, writableSpotMarketIndexes: [marketIndex, QUOTE_SPOT_MARKET_INDEX], }); if (makerInfo) { remainingAccounts.push({ pubkey: makerInfo.maker, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: makerInfo.makerStats, isWritable: true, isSigner: false, }); } if (referrerInfo) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } const orderId = order.orderId; const makerOrderId = makerInfo ? makerInfo.order.orderId : null; this.addSpotFulfillmentAccounts( marketIndex, remainingAccounts, fulfillmentConfig ); return await this.program.instruction.fillSpotOrder( orderId, fulfillmentConfig ? fulfillmentConfig.fulfillmentType : null, makerOrderId, { accounts: { state: await this.getStatePublicKey(), filler, fillerStats: fillerStatsPublicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, } ); } addSpotFulfillmentAccounts( marketIndex: number, remainingAccounts: AccountMeta[], fulfillmentConfig?: | SerumV3FulfillmentConfigAccount | PhoenixV1FulfillmentConfigAccount ): void { if (fulfillmentConfig) { if ('serumProgramId' in fulfillmentConfig) { this.addSerumRemainingAccounts( marketIndex, remainingAccounts, fulfillmentConfig ); } else if ('phoenixProgramId' in fulfillmentConfig) { this.addPhoenixRemainingAccounts( marketIndex, remainingAccounts, fulfillmentConfig ); } else { throw Error('Invalid fulfillment config type'); } } else { remainingAccounts.push({ pubkey: this.getSpotMarketAccount(marketIndex).vault, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: this.getQuoteSpotMarketAccount().vault, isWritable: false, isSigner: false, }); } } addSerumRemainingAccounts( marketIndex: number, remainingAccounts: AccountMeta[], fulfillmentConfig: SerumV3FulfillmentConfigAccount ): void { remainingAccounts.push({ pubkey: fulfillmentConfig.pubkey, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumProgramId, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumMarket, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumRequestQueue, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumEventQueue, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumBids, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumAsks, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumBaseVault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumQuoteVault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.serumOpenOrders, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: getSerumSignerPublicKey( fulfillmentConfig.serumProgramId, fulfillmentConfig.serumMarket, fulfillmentConfig.serumSignerNonce ), isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: this.getSignerPublicKey(), isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: this.getSpotMarketAccount(marketIndex).vault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: this.getQuoteSpotMarketAccount().vault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: this.getStateAccount().srmVault, isWritable: false, isSigner: false, }); } addPhoenixRemainingAccounts( marketIndex: number, remainingAccounts: AccountMeta[], fulfillmentConfig: PhoenixV1FulfillmentConfigAccount ): void { remainingAccounts.push({ pubkey: fulfillmentConfig.pubkey, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.phoenixProgramId, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.phoenixLogAuthority, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.phoenixMarket, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: this.getSignerPublicKey(), isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.phoenixBaseVault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: fulfillmentConfig.phoenixQuoteVault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: this.getSpotMarketAccount(marketIndex).vault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: this.getQuoteSpotMarketAccount().vault, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }); } /** * Swap tokens in drift account using jupiter * @param jupiterClient jupiter client to find routes and jupiter instructions * @param outMarketIndex the market index of the token you're buying * @param inMarketIndex the market index of the token you're selling * @param outAssociatedTokenAccount the token account to receive the token being sold on jupiter * @param inAssociatedTokenAccount the token account to * @param amount the amount of TokenIn, regardless of swapMode * @param slippageBps the max slippage passed to jupiter api * @param swapMode jupiter swapMode (ExactIn or ExactOut), default is ExactIn * @param route the jupiter route to use for the swap * @param reduceOnly specify if In or Out token on the drift account must reduceOnly, checked at end of swap * @param txParams */ public async swap({ jupiterClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, inAssociatedTokenAccount, amount, slippageBps, swapMode, route, reduceOnly, txParams, v6, onlyDirectRoutes = false, }: { jupiterClient: JupiterClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; inAssociatedTokenAccount?: PublicKey; amount: BN; slippageBps?: number; swapMode?: SwapMode; route?: Route; reduceOnly?: SwapReduceOnly; txParams?: TxParams; onlyDirectRoutes?: boolean; v6?: { quote?: QuoteResponse; }; }): Promise { let ixs: anchor.web3.TransactionInstruction[]; let lookupTables: anchor.web3.AddressLookupTableAccount[]; if (v6) { const res = await this.getJupiterSwapIxV6({ jupiterClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, inAssociatedTokenAccount, amount, slippageBps, swapMode, quote: v6.quote, reduceOnly, onlyDirectRoutes, }); ixs = res.ixs; lookupTables = res.lookupTables; } else { const res = await this.getJupiterSwapIx({ jupiterClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, inAssociatedTokenAccount, amount, slippageBps, swapMode, route, reduceOnly, }); ixs = res.ixs; lookupTables = res.lookupTables; } const tx = (await this.buildTransaction( ixs, txParams, 0, lookupTables )) as VersionedTransaction; const { txSig, slot } = await this.sendTransaction(tx); this.spotMarketLastSlotCache.set(outMarketIndex, slot); this.spotMarketLastSlotCache.set(inMarketIndex, slot); return txSig; } public async getJupiterSwapIx({ jupiterClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, inAssociatedTokenAccount, amount, slippageBps, swapMode, onlyDirectRoutes, route, reduceOnly, userAccountPublicKey, }: { jupiterClient: JupiterClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; inAssociatedTokenAccount?: PublicKey; amount: BN; slippageBps?: number; swapMode?: SwapMode; onlyDirectRoutes?: boolean; route?: Route; reduceOnly?: SwapReduceOnly; userAccountPublicKey?: PublicKey; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; }> { const outMarket = this.getSpotMarketAccount(outMarketIndex); const inMarket = this.getSpotMarketAccount(inMarketIndex); if (!route) { const routes = await jupiterClient.getRoutes({ inputMint: inMarket.mint, outputMint: outMarket.mint, amount, slippageBps, swapMode, onlyDirectRoutes, }); if (!routes || routes.length === 0) { throw new Error('No jupiter routes found'); } route = routes[0]; } const transaction = await jupiterClient.getSwapTransaction({ route, userPublicKey: this.provider.wallet.publicKey, slippageBps, }); const { transactionMessage, lookupTables } = await jupiterClient.getTransactionMessageAndLookupTables({ transaction, }); const jupiterInstructions = jupiterClient.getJupiterInstructions({ transactionMessage, inputMint: inMarket.mint, outputMint: outMarket.mint, }); const preInstructions = []; if (!outAssociatedTokenAccount) { outAssociatedTokenAccount = await this.getAssociatedTokenAccount( outMarket.marketIndex, false ); const accountInfo = await this.connection.getAccountInfo( outAssociatedTokenAccount ); if (!accountInfo) { preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( outAssociatedTokenAccount, this.provider.wallet.publicKey, this.provider.wallet.publicKey, outMarket.mint ) ); } } if (!inAssociatedTokenAccount) { inAssociatedTokenAccount = await this.getAssociatedTokenAccount( inMarket.marketIndex, false ); const accountInfo = await this.connection.getAccountInfo( inAssociatedTokenAccount ); if (!accountInfo) { preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( inAssociatedTokenAccount, this.provider.wallet.publicKey, this.provider.wallet.publicKey, inMarket.mint ) ); } } const { beginSwapIx, endSwapIx } = await this.getSwapIx({ outMarketIndex, inMarketIndex, amountIn: amount, inTokenAccount: inAssociatedTokenAccount, outTokenAccount: outAssociatedTokenAccount, reduceOnly, userAccountPublicKey, }); const ixs = [ ...preInstructions, beginSwapIx, ...jupiterInstructions, endSwapIx, ]; return { ixs, lookupTables }; } public async getJupiterSwapIxV6({ jupiterClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, inAssociatedTokenAccount, amount, slippageBps, swapMode, onlyDirectRoutes, quote, reduceOnly, userAccountPublicKey, }: { jupiterClient: JupiterClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; inAssociatedTokenAccount?: PublicKey; amount: BN; slippageBps?: number; swapMode?: SwapMode; onlyDirectRoutes?: boolean; quote?: QuoteResponse; reduceOnly?: SwapReduceOnly; userAccountPublicKey?: PublicKey; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; }> { const outMarket = this.getSpotMarketAccount(outMarketIndex); const inMarket = this.getSpotMarketAccount(inMarketIndex); if (!quote) { const fetchedQuote = await jupiterClient.getQuote({ inputMint: inMarket.mint, outputMint: outMarket.mint, amount, slippageBps, swapMode, onlyDirectRoutes, }); quote = fetchedQuote; } if (!quote) { throw new Error("Could not fetch Jupiter's quote. Please try again."); } const transaction = await jupiterClient.getSwap({ quote, userPublicKey: this.provider.wallet.publicKey, slippageBps, }); const { transactionMessage, lookupTables } = await jupiterClient.getTransactionMessageAndLookupTables({ transaction, }); const jupiterInstructions = jupiterClient.getJupiterInstructions({ transactionMessage, inputMint: inMarket.mint, outputMint: outMarket.mint, }); const preInstructions = []; if (!outAssociatedTokenAccount) { outAssociatedTokenAccount = await this.getAssociatedTokenAccount( outMarket.marketIndex, false ); const accountInfo = await this.connection.getAccountInfo( outAssociatedTokenAccount ); if (!accountInfo) { preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( outAssociatedTokenAccount, this.provider.wallet.publicKey, this.provider.wallet.publicKey, outMarket.mint ) ); } } if (!inAssociatedTokenAccount) { inAssociatedTokenAccount = await this.getAssociatedTokenAccount( inMarket.marketIndex, false ); const accountInfo = await this.connection.getAccountInfo( inAssociatedTokenAccount ); if (!accountInfo) { preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( inAssociatedTokenAccount, this.provider.wallet.publicKey, this.provider.wallet.publicKey, inMarket.mint ) ); } } const { beginSwapIx, endSwapIx } = await this.getSwapIx({ outMarketIndex, inMarketIndex, amountIn: amount, inTokenAccount: inAssociatedTokenAccount, outTokenAccount: outAssociatedTokenAccount, reduceOnly, userAccountPublicKey, }); const ixs = [ ...preInstructions, beginSwapIx, ...jupiterInstructions, endSwapIx, ]; return { ixs, lookupTables }; } /** * Get the drift begin_swap and end_swap instructions * * @param outMarketIndex the market index of the token you're buying * @param inMarketIndex the market index of the token you're selling * @param amountIn the amount of the token to sell * @param inTokenAccount the token account to move the tokens being sold * @param outTokenAccount the token account to receive the tokens being bought * @param limitPrice the limit price of the swap * @param reduceOnly * @param userAccountPublicKey optional, specify a custom userAccountPublicKey to use instead of getting the current user account; can be helpful if the account is being created within the current tx */ public async getSwapIx({ outMarketIndex, inMarketIndex, amountIn, inTokenAccount, outTokenAccount, limitPrice, reduceOnly, userAccountPublicKey, }: { outMarketIndex: number; inMarketIndex: number; amountIn: BN; inTokenAccount: PublicKey; outTokenAccount: PublicKey; limitPrice?: BN; reduceOnly?: SwapReduceOnly; userAccountPublicKey?: PublicKey; }): Promise<{ beginSwapIx: TransactionInstruction; endSwapIx: TransactionInstruction; }> { const userAccountPublicKeyToUse = userAccountPublicKey || (await this.getUserAccountPublicKey()); const userAccounts = []; try { if (this.hasUser() && this.getUser().getUserAccountAndSlot()) { userAccounts.push(this.getUser().getUserAccountAndSlot()!.data); } } catch (err) { // ignore } const remainingAccounts = this.getRemainingAccounts({ userAccounts, writableSpotMarketIndexes: [outMarketIndex, inMarketIndex], readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); const beginSwapIx = await this.program.instruction.beginSwap( inMarketIndex, outMarketIndex, amountIn, { accounts: { state: await this.getStatePublicKey(), user: userAccountPublicKeyToUse, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, outSpotMarketVault: outSpotMarket.vault, inSpotMarketVault: inSpotMarket.vault, inTokenAccount, outTokenAccount, tokenProgram: TOKEN_PROGRAM_ID, driftSigner: this.getStateAccount().signer, instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, }, remainingAccounts, } ); const endSwapIx = await this.program.instruction.endSwap( inMarketIndex, outMarketIndex, limitPrice ?? null, reduceOnly ?? null, { accounts: { state: await this.getStatePublicKey(), user: userAccountPublicKeyToUse, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, outSpotMarketVault: outSpotMarket.vault, inSpotMarketVault: inSpotMarket.vault, inTokenAccount, outTokenAccount, tokenProgram: TOKEN_PROGRAM_ID, driftSigner: this.getStateAccount().signer, instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, }, remainingAccounts, } ); return { beginSwapIx, endSwapIx }; } public async stakeForMSOL({ amount }: { amount: BN }): Promise { const ixs = await this.getStakeForMSOLIx({ amount }); const tx = await this.buildTransaction(ixs); return this.sendTransaction(tx); } public async getStakeForMSOLIx({ amount, userAccountPublicKey, }: { amount: BN; userAccountPublicKey?: PublicKey; }): Promise { const wSOLMint = this.getSpotMarketAccount(1).mint; const mSOLAccount = await this.getAssociatedTokenAccount(2); const wSOLAccount = await this.getAssociatedTokenAccount(1, false); const wSOLAccountExists = await this.checkIfAccountExists(wSOLAccount); const closeWSOLIx = createCloseAccountInstruction( wSOLAccount, this.wallet.publicKey, this.wallet.publicKey ); const createWSOLIx = await this.createAssociatedTokenAccountIdempotentInstruction( wSOLAccount, this.wallet.publicKey, this.wallet.publicKey, wSOLMint ); const { beginSwapIx, endSwapIx } = await this.getSwapIx({ inMarketIndex: 1, outMarketIndex: 2, amountIn: amount, inTokenAccount: wSOLAccount, outTokenAccount: mSOLAccount, userAccountPublicKey, }); const program = getMarinadeFinanceProgram(this.provider); const depositIx = await getMarinadeDepositIx({ program, mSOLAccount: mSOLAccount, transferFrom: this.wallet.publicKey, amount, }); const ixs = []; if (!wSOLAccountExists) { ixs.push(createWSOLIx); } ixs.push(beginSwapIx, closeWSOLIx, depositIx, createWSOLIx, endSwapIx); return ixs; } public async triggerOrder( userAccountPublicKey: PublicKey, user: UserAccount, order: Order, txParams?: TxParams, fillerPublicKey?: PublicKey ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getTriggerOrderIx( userAccountPublicKey, user, order, fillerPublicKey ), txParams ), [], this.opts ); return txSig; } public async getTriggerOrderIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, order: Order, fillerPublicKey?: PublicKey ): Promise { const filler = fillerPublicKey ?? (await this.getUserAccountPublicKey()); let remainingAccountsParams; if (isVariant(order.marketType, 'perp')) { remainingAccountsParams = { userAccounts: [userAccount], writablePerpMarketIndexes: [order.marketIndex], }; } else { remainingAccountsParams = { userAccounts: [userAccount], writableSpotMarketIndexes: [order.marketIndex, QUOTE_SPOT_MARKET_INDEX], }; } const remainingAccounts = this.getRemainingAccounts( remainingAccountsParams ); const orderId = order.orderId; return await this.program.instruction.triggerOrder(orderId, { accounts: { state: await this.getStatePublicKey(), filler, user: userAccountPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async forceCancelOrders( userAccountPublicKey: PublicKey, user: UserAccount, txParams?: TxParams, fillerPublicKey?: PublicKey ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getForceCancelOrdersIx( userAccountPublicKey, user, fillerPublicKey ), txParams ), [], this.opts ); return txSig; } public async getForceCancelOrdersIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, fillerPublicKey?: PublicKey ): Promise { const filler = fillerPublicKey ?? (await this.getUserAccountPublicKey()); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [userAccount], writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); return await this.program.instruction.forceCancelOrders({ accounts: { state: await this.getStatePublicKey(), filler, user: userAccountPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async updateUserIdle( userAccountPublicKey: PublicKey, user: UserAccount, txParams?: TxParams, fillerPublicKey?: PublicKey ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getUpdateUserIdleIx( userAccountPublicKey, user, fillerPublicKey ), txParams ), [], this.opts ); return txSig; } public async getUpdateUserIdleIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, fillerPublicKey?: PublicKey ): Promise { const filler = fillerPublicKey ?? (await this.getUserAccountPublicKey()); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [userAccount], }); return await this.program.instruction.updateUserIdle({ accounts: { state: await this.getStatePublicKey(), filler, user: userAccountPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async updateUserOpenOrdersCount( userAccountPublicKey: PublicKey, user: UserAccount, txParams?: TxParams, fillerPublicKey?: PublicKey ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getUpdateUserOpenOrdersCountIx( userAccountPublicKey, user, fillerPublicKey ), txParams ), [], this.opts ); return txSig; } public async getUpdateUserOpenOrdersCountIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, fillerPublicKey?: PublicKey ): Promise { const filler = fillerPublicKey ?? (await this.getUserAccountPublicKey()); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [userAccount], }); return await this.program.instruction.updateUserOpenOrdersCount({ accounts: { state: await this.getStatePublicKey(), filler, user: userAccountPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, }); } public async placeAndTakePerpOrder( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], referrerInfo?: ReferrerInfo, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlaceAndTakePerpOrderIx( orderParams, makerInfo, referrerInfo, subAccountId ), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } public async placeAndTakePerpWithAdditionalOrders( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], referrerInfo?: ReferrerInfo, bracketOrdersParams = new Array(), txParams?: TxParams, subAccountId?: number, cancelExistingOrders?: boolean, settlePnl?: boolean ): Promise<{ txSig: TransactionSignature; signedCancelExistingOrdersTx?: Transaction; signedSettlePnlTx?: Transaction; }> { let cancelExistingOrdersTx: Transaction; if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { const cancelOrdersIx = await this.getCancelOrdersIx( orderParams.marketType, orderParams.marketIndex, null, subAccountId ); //@ts-ignore cancelExistingOrdersTx = await this.buildTransaction( [cancelOrdersIx], txParams, this.txVersion ); } /* Settle PnL after fill if requested */ let settlePnlTx: Transaction; if (settlePnl && isVariant(orderParams.marketType, 'perp')) { const userAccountPublicKey = await this.getUserAccountPublicKey( subAccountId ); const settlePnlIx = await this.settlePNLIx( userAccountPublicKey, this.getUserAccount(subAccountId), orderParams.marketIndex ); //@ts-ignore settlePnlTx = await this.buildTransaction( [settlePnlIx], txParams, this.txVersion ); } const ixs = []; const placeAndTakeIx = await this.getPlaceAndTakePerpOrderIx( orderParams, makerInfo, referrerInfo, subAccountId ); ixs.push(placeAndTakeIx); if (bracketOrdersParams.length > 0) { const bracketOrdersIx = await this.getPlaceOrdersIx( bracketOrdersParams, subAccountId ); ixs.push(bracketOrdersIx); } const placeAndTakeTx = await this.buildTransaction(ixs, txParams); const allPossibleTxs = [ placeAndTakeTx, cancelExistingOrdersTx, settlePnlTx, ]; const txKeys = [ 'signedPlaceAndTakeTx', 'signedCancelExistingOrdersTx', 'signedSettlePnlTx', ]; const { signedPlaceAndTakeTx, signedCancelExistingOrdersTx, signedSettlePnlTx, } = await getSignedTransactionMap( //@ts-ignore this.provider.wallet, allPossibleTxs, txKeys ); const { txSig, slot } = await this.sendTransaction( signedPlaceAndTakeTx, [], this.opts, true ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); //@ts-ignore return { txSig, signedCancelExistingOrdersTx, signedSettlePnlTx }; } public async getPlaceAndTakePerpOrderIx( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], referrerInfo?: ReferrerInfo, subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.PERP }); const userStatsPublicKey = await this.getUserStatsAccountPublicKey(); const user = await this.getUserAccountPublicKey(subAccountId); makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo ? [makerInfo] : []; const userAccounts = [this.getUserAccount(subAccountId)]; for (const maker of makerInfo) { userAccounts.push(maker.makerUserAccount); } const remainingAccounts = this.getRemainingAccounts({ userAccounts, useMarketLastSlotCache: true, writablePerpMarketIndexes: [orderParams.marketIndex], }); for (const maker of makerInfo) { remainingAccounts.push({ pubkey: maker.maker, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: maker.makerStats, isWritable: true, isSigner: false, }); } if (referrerInfo) { const referrerIsMaker = makerInfo.find((maker) => maker.maker.equals(referrerInfo.referrer)) !== undefined; if (!referrerIsMaker) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } } return await this.program.instruction.placeAndTakePerpOrder( orderParams, null, { accounts: { state: await this.getStatePublicKey(), user, userStats: userStatsPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, } ); } public async placeAndMakePerpOrder( orderParams: OptionalOrderParams, takerInfo: TakerInfo, referrerInfo?: ReferrerInfo, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlaceAndMakePerpOrderIx( orderParams, takerInfo, referrerInfo, subAccountId ), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } public async getPlaceAndMakePerpOrderIx( orderParams: OptionalOrderParams, takerInfo: TakerInfo, referrerInfo?: ReferrerInfo, subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.PERP }); const userStatsPublicKey = this.getUserStatsAccountPublicKey(); const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [ this.getUserAccount(subAccountId), takerInfo.takerUserAccount, ], useMarketLastSlotCache: true, writablePerpMarketIndexes: [orderParams.marketIndex], }); if (referrerInfo) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } const takerOrderId = takerInfo.order.orderId; return await this.program.instruction.placeAndMakePerpOrder( orderParams, takerOrderId, { accounts: { state: await this.getStatePublicKey(), user, userStats: userStatsPublicKey, taker: takerInfo.taker, takerStats: takerInfo.takerStats, authority: this.wallet.publicKey, }, remainingAccounts, } ); } public async placeAndTakeSpotOrder( orderParams: OptionalOrderParams, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, makerInfo?: MakerInfo, referrerInfo?: ReferrerInfo, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlaceAndTakeSpotOrderIx( orderParams, fulfillmentConfig, makerInfo, referrerInfo, subAccountId ), txParams ), [], this.opts ); this.spotMarketLastSlotCache.set(orderParams.marketIndex, slot); this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } public async getPlaceAndTakeSpotOrderIx( orderParams: OptionalOrderParams, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, makerInfo?: MakerInfo, referrerInfo?: ReferrerInfo, subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.SPOT }); const userStatsPublicKey = this.getUserStatsAccountPublicKey(); const user = await this.getUserAccountPublicKey(subAccountId); const userAccounts = [this.getUserAccount(subAccountId)]; if (makerInfo !== undefined) { userAccounts.push(makerInfo.makerUserAccount); } const remainingAccounts = this.getRemainingAccounts({ userAccounts, useMarketLastSlotCache: true, writableSpotMarketIndexes: [ orderParams.marketIndex, QUOTE_SPOT_MARKET_INDEX, ], }); let makerOrderId = null; if (makerInfo) { makerOrderId = makerInfo.order.orderId; remainingAccounts.push({ pubkey: makerInfo.maker, isSigner: false, isWritable: true, }); remainingAccounts.push({ pubkey: makerInfo.makerStats, isSigner: false, isWritable: true, }); } if (referrerInfo) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } this.addSpotFulfillmentAccounts( orderParams.marketIndex, remainingAccounts, fulfillmentConfig ); return await this.program.instruction.placeAndTakeSpotOrder( orderParams, fulfillmentConfig ? fulfillmentConfig.fulfillmentType : null, makerOrderId, { accounts: { state: await this.getStatePublicKey(), user, userStats: userStatsPublicKey, authority: this.wallet.publicKey, }, remainingAccounts, } ); } public async placeAndMakeSpotOrder( orderParams: OptionalOrderParams, takerInfo: TakerInfo, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, referrerInfo?: ReferrerInfo, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlaceAndMakeSpotOrderIx( orderParams, takerInfo, fulfillmentConfig, referrerInfo, subAccountId ), txParams ), [], this.opts ); this.spotMarketLastSlotCache.set(orderParams.marketIndex, slot); this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } public async getPlaceAndMakeSpotOrderIx( orderParams: OptionalOrderParams, takerInfo: TakerInfo, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, referrerInfo?: ReferrerInfo, subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.SPOT }); const userStatsPublicKey = this.getUserStatsAccountPublicKey(); const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [ this.getUserAccount(subAccountId), takerInfo.takerUserAccount, ], useMarketLastSlotCache: true, writableSpotMarketIndexes: [ orderParams.marketIndex, QUOTE_SPOT_MARKET_INDEX, ], }); if (referrerInfo) { remainingAccounts.push({ pubkey: referrerInfo.referrer, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: referrerInfo.referrerStats, isWritable: true, isSigner: false, }); } this.addSpotFulfillmentAccounts( orderParams.marketIndex, remainingAccounts, fulfillmentConfig ); const takerOrderId = takerInfo.order.orderId; return await this.program.instruction.placeAndMakeSpotOrder( orderParams, takerOrderId, fulfillmentConfig ? fulfillmentConfig.fulfillmentType : null, { accounts: { state: await this.getStatePublicKey(), user, userStats: userStatsPublicKey, taker: takerInfo.taker, takerStats: takerInfo.takerStats, authority: this.wallet.publicKey, }, remainingAccounts, } ); } /** * @deprecated use {@link placePerpOrder} or {@link placeAndTakePerpOrder} instead */ public async closePosition( marketIndex: number, limitPrice?: BN, subAccountId?: number ): Promise { const userPosition = this.getUser(subAccountId).getPerpPosition(marketIndex); if (!userPosition) { throw Error(`No position in market ${marketIndex.toString()}`); } return await this.placeAndTakePerpOrder( { orderType: OrderType.MARKET, marketIndex, direction: findDirectionToClose(userPosition), baseAssetAmount: userPosition.baseAssetAmount.abs(), reduceOnly: true, price: limitPrice, }, undefined, undefined, undefined, subAccountId ); } /** * Modifies an open order by closing it and replacing it with a new order. * @deprecated use modifyOrder instead * @param orderId: The open order to modify * @param newBaseAmount: The new base amount for the order. One of [newBaseAmount|newLimitPrice|newOraclePriceOffset] must be provided. * @param newLimitPice: The new limit price for the order. One of [newBaseAmount|newLimitPrice|newOraclePriceOffset] must be provided. * @param newOraclePriceOffset: The new oracle price offset for the order. One of [newBaseAmount|newLimitPrice|newOraclePriceOffset] must be provided. * @returns */ public async modifyPerpOrder( orderId: number, newBaseAmount?: BN, newLimitPrice?: BN, newOraclePriceOffset?: number ): Promise { return this.modifyOrder({ orderId, newBaseAmount, newLimitPrice, newOraclePriceOffset, }); } /** * Modifies an open order by closing it and replacing it with a new order. * @deprecated use modifyOrderByUserOrderId instead * @param userOrderId: The open order to modify * @param newBaseAmount: The new base amount for the order. One of [newBaseAmount|newLimitPrice|newOraclePriceOffset] must be provided. * @param newLimitPice: The new limit price for the order. One of [newBaseAmount|newLimitPrice|newOraclePriceOffset] must be provided. * @param newOraclePriceOffset: The new oracle price offset for the order. One of [newBaseAmount|newLimitPrice|newOraclePriceOffset] must be provided. * @returns */ public async modifyPerpOrderByUserOrderId( userOrderId: number, newBaseAmount?: BN, newLimitPrice?: BN, newOraclePriceOffset?: number ): Promise { return this.modifyOrderByUserOrderId({ userOrderId, newBaseAmount, newLimitPrice, newOraclePriceOffset, }); } /** * Modifies an open order (spot or perp) by closing it and replacing it with a new order. * @param orderParams.orderId: The open order to modify * @param orderParams.newDirection: The new direction for the order * @param orderParams.newBaseAmount: The new base amount for the order * @param orderParams.newLimitPice: The new limit price for the order * @param orderParams.newOraclePriceOffset: The new oracle price offset for the order * @param orderParams.newTriggerPrice: Optional - Thew new trigger price for the order. * @param orderParams.auctionDuration: * @param orderParams.auctionStartPrice: * @param orderParams.auctionEndPrice: * @param orderParams.reduceOnly: * @param orderParams.postOnly: * @param orderParams.immediateOrCancel: * @param orderParams.policy: * @param orderParams.maxTs: * @returns */ public async modifyOrder( orderParams: { orderId: number; newDirection?: PositionDirection; newBaseAmount?: BN; newLimitPrice?: BN; newOraclePriceOffset?: number; newTriggerPrice?: BN; newTriggerCondition?: OrderTriggerCondition; auctionDuration?: number; auctionStartPrice?: BN; auctionEndPrice?: BN; reduceOnly?: boolean; postOnly?: boolean; immediateOrCancel?: boolean; maxTs?: BN; policy?: ModifyOrderPolicy; }, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getModifyOrderIx(orderParams, subAccountId), txParams ), [], this.opts ); return txSig; } public async getModifyOrderIx( { orderId, newDirection, newBaseAmount, newLimitPrice, newOraclePriceOffset, newTriggerPrice, newTriggerCondition, auctionDuration, auctionStartPrice, auctionEndPrice, reduceOnly, postOnly, immediateOrCancel, maxTs, policy, }: { orderId: number; newDirection?: PositionDirection; newBaseAmount?: BN; newLimitPrice?: BN; newOraclePriceOffset?: number; newTriggerPrice?: BN; newTriggerCondition?: OrderTriggerCondition; auctionDuration?: number; auctionStartPrice?: BN; auctionEndPrice?: BN; reduceOnly?: boolean; postOnly?: boolean; immediateOrCancel?: boolean; maxTs?: BN; policy?: ModifyOrderPolicy; }, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, }); const orderParams: ModifyOrderParams = { baseAssetAmount: newBaseAmount || null, direction: newDirection || null, price: newLimitPrice || null, oraclePriceOffset: newOraclePriceOffset || null, triggerPrice: newTriggerPrice || null, triggerCondition: newTriggerCondition || null, auctionDuration: auctionDuration || 0, auctionStartPrice: auctionStartPrice || ZERO, auctionEndPrice: auctionEndPrice || ZERO, reduceOnly: reduceOnly != undefined ? reduceOnly : null, postOnly: postOnly != undefined ? postOnly : null, immediateOrCancel: immediateOrCancel != undefined ? immediateOrCancel : null, policy: policy || null, maxTs: maxTs || null, }; return await this.program.instruction.modifyOrder(orderId, orderParams, { accounts: { state: await this.getStatePublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, }); } /** * Modifies an open order by closing it and replacing it with a new order. * @param orderParams.userOrderId: The open order to modify * @param orderParams.newDirection: The new direction for the order * @param orderParams.newBaseAmount: The new base amount for the order * @param orderParams.newLimitPice: The new limit price for the order * @param orderParams.newOraclePriceOffset: The new oracle price offset for the order * @param orderParams.newTriggerPrice: Optional - Thew new trigger price for the order. * @param orderParams.auctionDuration: Only required if order type changed to market from something else * @param orderParams.auctionStartPrice: Only required if order type changed to market from something else * @param orderParams.auctionEndPrice: Only required if order type changed to market from something else * @param orderParams.reduceOnly: * @param orderParams.postOnly: * @param orderParams.immediateOrCancel: * @param orderParams.policy: * @param orderParams.maxTs: * @returns */ public async modifyOrderByUserOrderId( orderParams: { userOrderId: number; newDirection?: PositionDirection; newBaseAmount?: BN; newLimitPrice?: BN; newOraclePriceOffset?: number; newTriggerPrice?: BN; newTriggerCondition?: OrderTriggerCondition; auctionDuration?: number; auctionStartPrice?: BN; auctionEndPrice?: BN; reduceOnly?: boolean; postOnly?: boolean; immediateOrCancel?: boolean; policy?: ModifyOrderPolicy; maxTs?: BN; }, txParams?: TxParams, subAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getModifyOrderByUserIdIx(orderParams, subAccountId), txParams ), [], this.opts ); return txSig; } public async getModifyOrderByUserIdIx( { userOrderId, newDirection, newBaseAmount, newLimitPrice, newOraclePriceOffset, newTriggerPrice, newTriggerCondition, auctionDuration, auctionStartPrice, auctionEndPrice, reduceOnly, postOnly, immediateOrCancel, maxTs, policy, }: { userOrderId: number; newDirection?: PositionDirection; newBaseAmount?: BN; newLimitPrice?: BN; newOraclePriceOffset?: number; newTriggerPrice?: BN; newTriggerCondition?: OrderTriggerCondition; auctionDuration?: number; auctionStartPrice?: BN; auctionEndPrice?: BN; reduceOnly?: boolean; postOnly?: boolean; immediateOrCancel?: boolean; policy?: ModifyOrderPolicy; maxTs?: BN; txParams?: TxParams; }, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], useMarketLastSlotCache: true, }); const orderParams: ModifyOrderParams = { baseAssetAmount: newBaseAmount || null, direction: newDirection || null, price: newLimitPrice || null, oraclePriceOffset: newOraclePriceOffset || null, triggerPrice: newTriggerPrice || null, triggerCondition: newTriggerCondition || null, auctionDuration: auctionDuration || null, auctionStartPrice: auctionStartPrice || null, auctionEndPrice: auctionEndPrice || null, reduceOnly: reduceOnly || false, postOnly: postOnly || null, immediateOrCancel: immediateOrCancel || false, policy: policy || null, maxTs: maxTs || null, }; return await this.program.instruction.modifyOrderByUserId( userOrderId, orderParams, { accounts: { state: await this.getStatePublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, }, remainingAccounts, } ); } public async settlePNLs( users: { settleeUserAccountPublicKey: PublicKey; settleeUserAccount: UserAccount; }[], marketIndexes: number[] ): Promise { const ixs = await this.getSettlePNLsIxs(users, marketIndexes); const tx = await this.buildTransaction(ixs, { computeUnits: 1_000_000 }); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getSettlePNLsIxs( users: { settleeUserAccountPublicKey: PublicKey; settleeUserAccount: UserAccount; }[], marketIndexes: number[] ): Promise> { const ixs = []; for (const { settleeUserAccountPublicKey, settleeUserAccount } of users) { for (const marketIndex of marketIndexes) { ixs.push( await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, marketIndex ) ); } } return ixs; } public async settlePNL( settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, marketIndex: number, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, marketIndex ), txParams ), [], this.opts ); return txSig; } public async settlePNLIx( settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, marketIndex: number ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], writablePerpMarketIndexes: [marketIndex], writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); return await this.program.instruction.settlePnl(marketIndex, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: settleeUserAccountPublicKey, spotMarketVault: this.getQuoteSpotMarketAccount().vault, }, remainingAccounts: remainingAccounts, }); } public async liquidatePerp( userAccountPublicKey: PublicKey, userAccount: UserAccount, marketIndex: number, maxBaseAssetAmount: BN, limitPrice?: BN, txParams?: TxParams, liquidatorSubAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getLiquidatePerpIx( userAccountPublicKey, userAccount, marketIndex, maxBaseAssetAmount, limitPrice, liquidatorSubAccountId ), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(marketIndex, slot); return txSig; } public async getLiquidatePerpIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, marketIndex: number, maxBaseAssetAmount: BN, limitPrice?: BN, liquidatorSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const liquidator = await this.getUserAccountPublicKey( liquidatorSubAccountId ); const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], useMarketLastSlotCache: true, writablePerpMarketIndexes: [marketIndex], }); return await this.program.instruction.liquidatePerp( marketIndex, maxBaseAssetAmount, limitPrice ?? null, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, liquidator, liquidatorStats: liquidatorStatsPublicKey, }, remainingAccounts: remainingAccounts, } ); } public async liquidateSpot( userAccountPublicKey: PublicKey, userAccount: UserAccount, assetMarketIndex: number, liabilityMarketIndex: number, maxLiabilityTransfer: BN, limitPrice?: BN, txParams?: TxParams, liquidatorSubAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getLiquidateSpotIx( userAccountPublicKey, userAccount, assetMarketIndex, liabilityMarketIndex, maxLiabilityTransfer, limitPrice, liquidatorSubAccountId ), txParams ), [], this.opts ); this.spotMarketLastSlotCache.set(assetMarketIndex, slot); this.spotMarketLastSlotCache.set(liabilityMarketIndex, slot); return txSig; } public async getLiquidateSpotIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, assetMarketIndex: number, liabilityMarketIndex: number, maxLiabilityTransfer: BN, limitPrice?: BN, liquidatorSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const liquidator = await this.getUserAccountPublicKey( liquidatorSubAccountId ); const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], useMarketLastSlotCache: true, writableSpotMarketIndexes: [liabilityMarketIndex, assetMarketIndex], }); return await this.program.instruction.liquidateSpot( assetMarketIndex, liabilityMarketIndex, maxLiabilityTransfer, limitPrice || null, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, liquidator, liquidatorStats: liquidatorStatsPublicKey, }, remainingAccounts: remainingAccounts, } ); } public async liquidateBorrowForPerpPnl( userAccountPublicKey: PublicKey, userAccount: UserAccount, perpMarketIndex: number, liabilityMarketIndex: number, maxLiabilityTransfer: BN, limitPrice?: BN, txParams?: TxParams, liquidatorSubAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getLiquidateBorrowForPerpPnlIx( userAccountPublicKey, userAccount, perpMarketIndex, liabilityMarketIndex, maxLiabilityTransfer, limitPrice, liquidatorSubAccountId ), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(perpMarketIndex, slot); this.spotMarketLastSlotCache.set(liabilityMarketIndex, slot); return txSig; } public async getLiquidateBorrowForPerpPnlIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, perpMarketIndex: number, liabilityMarketIndex: number, maxLiabilityTransfer: BN, limitPrice?: BN, liquidatorSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const liquidator = await this.getUserAccountPublicKey( liquidatorSubAccountId ); const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], writablePerpMarketIndexes: [perpMarketIndex], writableSpotMarketIndexes: [liabilityMarketIndex], }); return await this.program.instruction.liquidateBorrowForPerpPnl( perpMarketIndex, liabilityMarketIndex, maxLiabilityTransfer, limitPrice || null, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, liquidator, liquidatorStats: liquidatorStatsPublicKey, }, remainingAccounts: remainingAccounts, } ); } public async liquidatePerpPnlForDeposit( userAccountPublicKey: PublicKey, userAccount: UserAccount, perpMarketIndex: number, assetMarketIndex: number, maxPnlTransfer: BN, limitPrice?: BN, txParams?: TxParams, liquidatorSubAccountId?: number ): Promise { const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getLiquidatePerpPnlForDepositIx( userAccountPublicKey, userAccount, perpMarketIndex, assetMarketIndex, maxPnlTransfer, limitPrice, liquidatorSubAccountId ), txParams ), [], this.opts ); this.perpMarketLastSlotCache.set(perpMarketIndex, slot); this.spotMarketLastSlotCache.set(assetMarketIndex, slot); return txSig; } public async getLiquidatePerpPnlForDepositIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, perpMarketIndex: number, assetMarketIndex: number, maxPnlTransfer: BN, limitPrice?: BN, liquidatorSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const liquidator = await this.getUserAccountPublicKey( liquidatorSubAccountId ); const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], writablePerpMarketIndexes: [perpMarketIndex], writableSpotMarketIndexes: [assetMarketIndex], }); return await this.program.instruction.liquidatePerpPnlForDeposit( perpMarketIndex, assetMarketIndex, maxPnlTransfer, limitPrice || null, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, liquidator, liquidatorStats: liquidatorStatsPublicKey, }, remainingAccounts: remainingAccounts, } ); } public async resolvePerpBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, marketIndex: number, txParams?: TxParams, liquidatorSubAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getResolvePerpBankruptcyIx( userAccountPublicKey, userAccount, marketIndex, liquidatorSubAccountId ), txParams ), [], this.opts ); return txSig; } public async getResolvePerpBankruptcyIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, marketIndex: number, liquidatorSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const liquidator = await this.getUserAccountPublicKey( liquidatorSubAccountId ); const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], writablePerpMarketIndexes: [marketIndex], writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); const spotMarket = this.getQuoteSpotMarketAccount(); return await this.program.instruction.resolvePerpBankruptcy( QUOTE_SPOT_MARKET_INDEX, marketIndex, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, liquidator, liquidatorStats: liquidatorStatsPublicKey, spotMarketVault: spotMarket.vault, insuranceFundVault: spotMarket.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts: remainingAccounts, } ); } public async resolveSpotBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, marketIndex: number, txParams?: TxParams, liquidatorSubAccountId?: number ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getResolveSpotBankruptcyIx( userAccountPublicKey, userAccount, marketIndex, liquidatorSubAccountId ), txParams ), [], this.opts ); return txSig; } public async getResolveSpotBankruptcyIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, marketIndex: number, liquidatorSubAccountId?: number ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, userAccount.authority ); const liquidator = await this.getUserAccountPublicKey( liquidatorSubAccountId ); const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], writableSpotMarketIndexes: [marketIndex], }); const spotMarket = this.getSpotMarketAccount(marketIndex); return await this.program.instruction.resolveSpotBankruptcy(marketIndex, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, user: userAccountPublicKey, userStats: userStatsPublicKey, liquidatorStats: liquidatorStatsPublicKey, liquidator, spotMarketVault: spotMarket.vault, insuranceFundVault: spotMarket.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts: remainingAccounts, }); } public async updateFundingRate( perpMarketIndex: number, oracle: PublicKey, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getUpdateFundingRateIx(perpMarketIndex, oracle), txParams ), [], this.opts ); return txSig; } public async getUpdateFundingRateIx( perpMarketIndex: number, oracle: PublicKey ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, perpMarketIndex ); return await this.program.instruction.updateFundingRate(perpMarketIndex, { accounts: { state: await this.getStatePublicKey(), perpMarket: perpMarketPublicKey, oracle: oracle, }, }); } public async updatePerpBidAskTwap( perpMarketIndex: number, makers: [PublicKey, PublicKey][], txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getUpdatePerpBidAskTwapIx(perpMarketIndex, makers), txParams ), [], this.opts ); return txSig; } public async getUpdatePerpBidAskTwapIx( perpMarketIndex: number, makers: [PublicKey, PublicKey][] ): Promise { const perpMarket = this.getPerpMarketAccount(perpMarketIndex); const remainingAccounts = []; for (const [maker, makerStats] of makers) { remainingAccounts.push({ pubkey: maker, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: makerStats, isWritable: false, isSigner: false, }); } return await this.program.instruction.updatePerpBidAskTwap({ accounts: { state: await this.getStatePublicKey(), perpMarket: perpMarket.pubkey, oracle: perpMarket.amm.oracle, authority: this.wallet.publicKey, keeperStats: this.getUserStatsAccountPublicKey(), }, remainingAccounts, }); } public async settleFundingPayment( userAccountPublicKey: PublicKey, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getSettleFundingPaymentIx(userAccountPublicKey), txParams ), [], this.opts ); return txSig; } public async getSettleFundingPaymentIx( userAccountPublicKey: PublicKey ): Promise { const userAccount = (await this.program.account.user.fetch( userAccountPublicKey )) as UserAccount; const writablePerpMarketIndexes = []; for (const position of userAccount.perpPositions) { if (!positionIsAvailable(position)) { writablePerpMarketIndexes.push(position.marketIndex); } } const remainingAccounts = this.getRemainingAccounts({ userAccounts: [userAccount], writablePerpMarketIndexes, }); return await this.program.instruction.settleFundingPayment({ accounts: { state: await this.getStatePublicKey(), user: userAccountPublicKey, }, remainingAccounts, }); } public triggerEvent(eventName: keyof DriftClientAccountEvents, data?: any) { this.eventEmitter.emit(eventName, data); } public getOracleDataForPerpMarket(marketIndex: number): OraclePriceData { const oracleKey = this.getPerpMarketAccount(marketIndex).amm.oracle; const oracleData = this.getOraclePriceDataAndSlot(oracleKey).data; return oracleData; } public getOracleDataForSpotMarket(marketIndex: number): OraclePriceData { const oracleKey = this.getSpotMarketAccount(marketIndex).oracle; const oracleData = this.getOraclePriceDataAndSlot(oracleKey).data; return oracleData; } public async initializeInsuranceFundStake( marketIndex: number, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getInitializeInsuranceFundStakeIx(marketIndex), txParams ), [], this.opts ); return txSig; } public async getInitializeInsuranceFundStakeIx( marketIndex: number ): Promise { const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( this.program.programId, this.wallet.publicKey, marketIndex ); return await this.program.instruction.initializeInsuranceFundStake( marketIndex, { accounts: { insuranceFundStake: ifStakeAccountPublicKey, spotMarket: this.getSpotMarketAccount(marketIndex).pubkey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, payer: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, state: await this.getStatePublicKey(), }, } ); } public async getAddInsuranceFundStakeIx( marketIndex: number, amount: BN, collateralAccountPublicKey: PublicKey ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( this.program.programId, this.wallet.publicKey, marketIndex ); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); const ix = this.program.instruction.addInsuranceFundStake( marketIndex, amount, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarket.pubkey, insuranceFundStake: ifStakeAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, spotMarketVault: spotMarket.vault, insuranceFundVault: spotMarket.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), userTokenAccount: collateralAccountPublicKey, tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts, } ); return ix; } /** * Add to an insurance fund stake and optionally initialize the account */ public async addInsuranceFundStake({ marketIndex, amount, collateralAccountPublicKey, initializeStakeAccount, fromSubaccount, }: { /** * Spot market index */ marketIndex: number; amount: BN; /** * The account where the funds to stake come from. Usually an associated token account */ collateralAccountPublicKey: PublicKey; /** * Add instructions to initialize the staking account -- required if its the first time the currrent authority has staked in this market */ initializeStakeAccount?: boolean; /** * Optional -- withdraw from current subaccount to fund stake amount, instead of wallet balance */ fromSubaccount?: boolean; }): Promise { const addIfStakeIxs = []; const additionalSigners: Array = []; const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const isSolMarket = spotMarketAccount.mint.equals(WRAPPED_SOL_MINT); const createWSOLTokenAccount = isSolMarket && collateralAccountPublicKey.equals(this.wallet.publicKey); let tokenAccount; if (createWSOLTokenAccount) { const { ixs, pubkey } = await this.getWrappedSolAccountCreationIxs( amount, true ); tokenAccount = pubkey; ixs.forEach((ix) => { addIfStakeIxs.push(ix); }); } else { tokenAccount = collateralAccountPublicKey; } if (fromSubaccount) { const withdrawIx = await this.getWithdrawIx( amount, marketIndex, tokenAccount ); addIfStakeIxs.push(withdrawIx); } if (initializeStakeAccount) { const initializeIx = await this.getInitializeInsuranceFundStakeIx( marketIndex ); addIfStakeIxs.push(initializeIx); } const addFundsIx = await this.getAddInsuranceFundStakeIx( marketIndex, amount, tokenAccount ); addIfStakeIxs.push(addFundsIx); if (createWSOLTokenAccount) { addIfStakeIxs.push( createCloseAccountInstruction( tokenAccount, this.wallet.publicKey, this.wallet.publicKey, [] ) ); } const tx = await this.buildTransaction(addIfStakeIxs); const { txSig } = await this.sendTransaction( tx, additionalSigners, this.opts ); return txSig; } public async requestRemoveInsuranceFundStake( marketIndex: number, amount: BN ): Promise { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( this.program.programId, this.wallet.publicKey, marketIndex ); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); const ix = await this.program.instruction.requestRemoveInsuranceFundStake( marketIndex, amount, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarketAccount.pubkey, insuranceFundStake: ifStakeAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, insuranceFundVault: spotMarketAccount.insuranceFund.vault, }, remainingAccounts, } ); const tx = await this.buildTransaction(ix); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async cancelRequestRemoveInsuranceFundStake( marketIndex: number ): Promise { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( this.program.programId, this.wallet.publicKey, marketIndex ); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); const ix = await this.program.instruction.cancelRequestRemoveInsuranceFundStake( marketIndex, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarketAccount.pubkey, insuranceFundStake: ifStakeAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, insuranceFundVault: spotMarketAccount.insuranceFund.vault, }, remainingAccounts, } ); const tx = await this.buildTransaction(ix); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async removeInsuranceFundStake( marketIndex: number, collateralAccountPublicKey: PublicKey ): Promise { const removeIfStakeIxs = []; const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( this.program.programId, this.wallet.publicKey, marketIndex ); const additionalSigners: Array = []; const isSolMarket = spotMarketAccount.mint.equals(WRAPPED_SOL_MINT); const createWSOLTokenAccount = isSolMarket && collateralAccountPublicKey.equals(this.wallet.publicKey); let tokenAccount; if (createWSOLTokenAccount) { const { ixs, pubkey } = await this.getWrappedSolAccountCreationIxs( ZERO, true ); tokenAccount = pubkey; ixs.forEach((ix) => { removeIfStakeIxs.push(ix); }); } else { tokenAccount = collateralAccountPublicKey; const tokenAccountExists = await this.checkIfAccountExists(tokenAccount); if (!tokenAccountExists) { const createTokenAccountIx = await this.createAssociatedTokenAccountIdempotentInstruction( tokenAccount, this.wallet.publicKey, this.wallet.publicKey, spotMarketAccount.mint ); removeIfStakeIxs.push(createTokenAccountIx); } } const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); const removeStakeIx = await this.program.instruction.removeInsuranceFundStake(marketIndex, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarketAccount.pubkey, insuranceFundStake: ifStakeAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, insuranceFundVault: spotMarketAccount.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), userTokenAccount: tokenAccount, tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts, }); removeIfStakeIxs.push(removeStakeIx); // Close the wrapped sol account at the end of the transaction if (createWSOLTokenAccount) { removeIfStakeIxs.push( createCloseAccountInstruction( tokenAccount, this.wallet.publicKey, this.wallet.publicKey, [] ) ); } const tx = await this.buildTransaction(removeIfStakeIxs); const { txSig } = await this.sendTransaction( tx, additionalSigners, this.opts ); return txSig; } public async settleRevenueToInsuranceFund( marketIndex: number ): Promise { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: [marketIndex], }); const ix = await this.program.instruction.settleRevenueToInsuranceFund( marketIndex, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarketAccount.pubkey, spotMarketVault: spotMarketAccount.vault, driftSigner: this.getSignerPublicKey(), insuranceFundVault: spotMarketAccount.insuranceFund.vault, tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts, } ); const tx = await this.buildTransaction(ix); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async resolvePerpPnlDeficit( spotMarketIndex: number, perpMarketIndex: number, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getResolvePerpPnlDeficitIx(spotMarketIndex, perpMarketIndex), txParams ), [], this.opts ); return txSig; } public async getResolvePerpPnlDeficitIx( spotMarketIndex: number, perpMarketIndex: number ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount()], useMarketLastSlotCache: true, writablePerpMarketIndexes: [perpMarketIndex], writableSpotMarketIndexes: [spotMarketIndex], }); const spotMarket = this.getSpotMarketAccount(spotMarketIndex); return await this.program.instruction.resolvePerpPnlDeficit( spotMarketIndex, perpMarketIndex, { accounts: { state: await this.getStatePublicKey(), authority: this.wallet.publicKey, spotMarketVault: spotMarket.vault, insuranceFundVault: spotMarket.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), tokenProgram: TOKEN_PROGRAM_ID, }, remainingAccounts: remainingAccounts, } ); } public async getDepositIntoSpotMarketRevenuePoolIx( marketIndex: number, amount: BN, userTokenAccountPublicKey: PublicKey ): Promise { const spotMarket = await this.getSpotMarketAccount(marketIndex); const ix = await this.program.instruction.depositIntoSpotMarketRevenuePool( amount, { accounts: { state: await this.getStatePublicKey(), spotMarket: spotMarket.pubkey, authority: this.wallet.publicKey, spotMarketVault: spotMarket.vault, userTokenAccount: userTokenAccountPublicKey, tokenProgram: TOKEN_PROGRAM_ID, }, } ); return ix; } public async depositIntoSpotMarketRevenuePool( marketIndex: number, amount: BN, userTokenAccountPublicKey: PublicKey ): Promise { const ix = await this.getDepositIntoSpotMarketRevenuePoolIx( marketIndex, amount, userTokenAccountPublicKey ); const tx = await this.buildTransaction([ix]); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public getPerpMarketExtendedInfo( marketIndex: number ): PerpMarketExtendedInfo { const marketAccount = this.getPerpMarketAccount(marketIndex); const quoteAccount = this.getSpotMarketAccount(QUOTE_SPOT_MARKET_INDEX); const extendedInfo: PerpMarketExtendedInfo = { marketIndex, minOrderSize: marketAccount.amm?.minOrderSize, marginMaintenance: marketAccount.marginRatioMaintenance, pnlPoolValue: getTokenAmount( marketAccount.pnlPool?.scaledBalance, quoteAccount, SpotBalanceType.DEPOSIT ), contractTier: marketAccount.contractTier, availableInsurance: calculateMarketMaxAvailableInsurance( marketAccount, quoteAccount ), }; return extendedInfo; } /** * Returns the market index and type for a given market name * E.g. "SOL-PERP" -> { marketIndex: 0, marketType: MarketType.PERP } * * @param name */ getMarketIndexAndType( name: string ): { marketIndex: number; marketType: MarketType } | undefined { name = name.toUpperCase(); for (const perpMarketAccount of this.getPerpMarketAccounts()) { if (decodeName(perpMarketAccount.name).toUpperCase() === name) { return { marketIndex: perpMarketAccount.marketIndex, marketType: MarketType.PERP, }; } } for (const spotMarketAccount of this.getSpotMarketAccounts()) { if (decodeName(spotMarketAccount.name).toUpperCase() === name) { return { marketIndex: spotMarketAccount.marketIndex, marketType: MarketType.SPOT, }; } } return undefined; } private handleSignedTransaction() { this.metricsEventEmitter.emit('txSigned'); } sendTransaction( tx: Transaction | VersionedTransaction, additionalSigners?: Array, opts?: ConfirmOptions, preSigned?: boolean ): Promise { const extraConfirmationOptions: ExtraConfirmationOptions = this .enableMetricsEvents ? { onSignedCb: this.handleSignedTransaction.bind(this), } : undefined; if (tx instanceof VersionedTransaction) { return this.txSender.sendVersionedTransaction( tx as VersionedTransaction, additionalSigners, opts, preSigned, extraConfirmationOptions ); } else { return this.txSender.send( tx as Transaction, additionalSigners, opts, preSigned, extraConfirmationOptions ); } } async buildTransaction( instructions: TransactionInstruction | TransactionInstruction[], txParams?: TxParams, txVersion?: TransactionVersion, lookupTables?: AddressLookupTableAccount[] ): Promise { const allIx = []; const computeUnits = txParams?.computeUnits ?? this.txParams.computeUnits; if (computeUnits !== 200_000) { allIx.push( ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits, }) ); } const computeUnitsPrice = txParams?.computeUnitsPrice ?? this.txParams.computeUnitsPrice; if (computeUnitsPrice !== 0) { allIx.push( ComputeBudgetProgram.setComputeUnitPrice({ microLamports: computeUnitsPrice, }) ); } if (Array.isArray(instructions)) { allIx.push(...instructions); } else { allIx.push(instructions); } txVersion = txVersion ?? this.txVersion; if (txVersion === 'legacy') { return new Transaction().add(...allIx); } else { const marketLookupTable = await this.fetchMarketLookupTableAccount(); lookupTables = lookupTables ? [...lookupTables, marketLookupTable] : [marketLookupTable]; const message = new TransactionMessage({ payerKey: this.provider.wallet.publicKey, recentBlockhash: ( await this.provider.connection.getRecentBlockhash( this.opts.preflightCommitment ) ).blockhash, instructions: allIx, }).compileToV0Message(lookupTables); return new VersionedTransaction(message); } } }