UNPKG

@drift-labs/sdk-browser

Version:
1,732 lines (1,545 loc) 277 kB
import * as anchor from '@coral-xyz/anchor'; import { AnchorProvider, BN, Idl, Program, ProgramAccount, } from '@coral-xyz/anchor'; import { Idl as Idl30, Program as Program30 } from '@coral-xyz/anchor-30'; import bs58 from 'bs58'; import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction, createInitializeAccountInstruction, getAssociatedTokenAddress, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, getMint, getTransferHook, getExtraAccountMetaAddress, getExtraAccountMetas, resolveExtraAccountMeta, } from '@solana/spl-token'; import { DriftClientMetricsEvents, HighLeverageModeConfig, isVariant, IWallet, MakerInfo, MappedRecord, MarketType, ModifyOrderParams, ModifyOrderPolicy, OpenbookV2FulfillmentConfigAccount, OptionalOrderParams, OracleSource, Order, OrderParams, OrderTriggerCondition, OrderType, PerpMarketAccount, PerpMarketExtendedInfo, PhoenixV1FulfillmentConfigAccount, PlaceAndTakeOrderSuccessCondition, PositionDirection, ReferrerInfo, ReferrerNameAccount, SerumV3FulfillmentConfigAccount, SettlePnlMode, SignedTxData, SpotBalanceType, SpotMarketAccount, SpotPosition, StateAccount, SwapReduceOnly, SignedMsgOrderParamsMessage, TakerInfo, TxParams, UserAccount, UserStatsAccount, ProtectedMakerModeConfig, SignedMsgOrderParamsDelegateMessage, TokenProgramFlag, PostOnlyParams, } from './types'; import driftIDL from './idl/drift.json'; import { AccountMeta, AddressLookupTableAccount, BlockhashWithExpiryBlockHeight, ConfirmOptions, Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, Signer, SystemProgram, SYSVAR_CLOCK_PUBKEY, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, TransactionInstruction, TransactionSignature, TransactionVersion, VersionedTransaction, } from '@solana/web3.js'; import { TokenFaucet } from './tokenFaucet'; import { EventEmitter } from 'events'; import StrictEventEmitter from 'strict-event-emitter-types'; import { getDriftSignerPublicKey, getDriftStateAccountPublicKey, getFuelOverflowAccountPublicKey, getHighLeverageModeConfigPublicKey, getInsuranceFundStakeAccountPublicKey, getOpenbookV2FulfillmentConfigPublicKey, getPerpMarketPublicKey, getPhoenixFulfillmentConfigPublicKey, getProtectedMakerModeConfigPublicKey, getPythLazerOraclePublicKey, getPythPullOraclePublicKey, getReferrerNamePublicKeySync, getSerumFulfillmentConfigPublicKey, getSerumSignerPublicKey, getSpotMarketPublicKey, getSignedMsgUserAccountPublicKey, getUserAccountPublicKey, getUserAccountPublicKeySync, getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, } from './addresses/pda'; import { DataAndSlot, DelistedMarketSetting, DriftClientAccountEvents, DriftClientAccountSubscriber, } from './accounts/types'; import { TxSender, TxSigAndSlot } from './tx/types'; import { BASE_PRECISION, GOV_SPOT_MARKET_INDEX, ONE, PERCENTAGE_PRECISION, PRICE_PRECISION, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, ZERO, } from './constants/numericConstants'; import { findDirectionToClose, positionIsAvailable } from './math/position'; import { getSignedTokenAmount, getTokenAmount } from './math/spotBalance'; import { decodeName, DEFAULT_USER_NAME, encodeName } from './userName'; import { MMOraclePriceData, OraclePriceData } from './oracles/types'; import { DriftClientConfig } from './driftClientConfig'; import { PollingDriftClientAccountSubscriber } from './accounts/pollingDriftClientAccountSubscriber'; import { RetryTxSender } from './tx/retryTxSender'; import { User } from './user'; import { UserSubscriptionConfig } from './userConfig'; import { configs, DRIFT_ORACLE_RECEIVER_ID, DEFAULT_CONFIRMATION_OPTS, DRIFT_PROGRAM_ID, DriftEnv, PYTH_LAZER_STORAGE_ACCOUNT_KEY, } 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, SwapMode, } from './jupiter/jupiterClient'; import { getNonIdleUserFilter } from './memcmp'; import { UserStatsSubscriptionConfig } from './userStatsConfig'; import { getMarinadeDepositIx, getMarinadeFinanceProgram } from './marinade'; import { getOrderParams, isUpdateHighLeverageMode } from './orderParams'; import { numberToSafeBN } from './math/utils'; import { TransactionParamProcessor } from './tx/txParamProcessor'; import { isOracleValid, trimVaaSignatures } from './math/oracles'; import { TxHandler } from './tx/txHandler'; import { DEFAULT_RECEIVER_PROGRAM_ID, wormholeCoreBridgeIdl, } from '@pythnetwork/pyth-solana-receiver'; import { parseAccumulatorUpdateData } from '@pythnetwork/price-service-sdk'; import { DEFAULT_WORMHOLE_PROGRAM_ID, getGuardianSetPda, } from '@pythnetwork/pyth-solana-receiver/lib/address'; import { WormholeCoreBridgeSolana } from '@pythnetwork/pyth-solana-receiver/lib/idl/wormhole_core_bridge_solana'; import { PythSolanaReceiver } from '@pythnetwork/pyth-solana-receiver/lib/idl/pyth_solana_receiver'; import { getFeedIdUint8Array, trimFeedId } from './util/pythOracleUtils'; import { createMinimalEd25519VerifyIx } from './util/ed25519Utils'; import { createNativeInstructionDiscriminatorBuffer, isVersionedTransaction, } from './tx/utils'; import pythSolanaReceiverIdl from './idl/pyth_solana_receiver.json'; import { asV0Tx, PullFeed, AnchorUtils } from '@switchboard-xyz/on-demand'; import { gprcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; import nacl from 'tweetnacl'; import { Slothash } from './slot/SlothashSubscriber'; import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; import { Commitment } from 'gill'; import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; 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; env: DriftEnv; opts?: ConfirmOptions; useHotWalletAdmin?: boolean; users = new Map<string, User>(); userStats?: UserStats; activeSubAccountId: number; userAccountSubscriptionConfig: UserSubscriptionConfig; userStatsAccountSubscriptionConfig: UserStatsSubscriptionConfig; accountSubscriber: DriftClientAccountSubscriber; eventEmitter: StrictEventEmitter<EventEmitter, DriftClientAccountEvents>; metricsEventEmitter: StrictEventEmitter< EventEmitter, DriftClientMetricsEvents >; _isSubscribed = false; txSender: TxSender; perpMarketLastSlotCache = new Map<number, number>(); spotMarketLastSlotCache = new Map<number, number>(); mustIncludePerpMarketIndexes = new Set<number>(); mustIncludeSpotMarketIndexes = new Set<number>(); authority: PublicKey; /** @deprecated use marketLookupTables */ marketLookupTable: PublicKey; /** @deprecated use lookupTableAccounts */ lookupTableAccount: AddressLookupTableAccount; marketLookupTables: PublicKey[]; lookupTableAccounts: AddressLookupTableAccount[]; includeDelegates?: boolean; authoritySubAccountMap?: Map<string, number[]>; skipLoadUsers?: boolean; txVersion: TransactionVersion; txParams: TxParams; enableMetricsEvents?: boolean; txHandler: TxHandler; receiverProgram?: Program<PythSolanaReceiver>; wormholeProgram?: Program<WormholeCoreBridgeSolana>; sbOnDemandProgramdId: PublicKey; sbOnDemandProgram?: Program30<Idl30>; sbProgramFeedConfigs?: Map<string, any>; 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.env = config.env ?? 'mainnet-beta'; this.opts = config.opts || { ...DEFAULT_CONFIRMATION_OPTS, }; this.useHotWalletAdmin = config.useHotWalletAdmin ?? false; if (config?.connection?.commitment) { // At the moment this ensures that our transaction simulations (which use Connection object) will use the same commitment level as our Transaction blockhashes (which use these opts) this.opts.commitment = config.connection.commitment; this.opts.preflightCommitment = config.connection.commitment; } this.provider = new AnchorProvider( config.connection, // @ts-ignore config.wallet, this.opts ); this.program = new Program( driftIDL as Idl, config.programID ?? new PublicKey(DRIFT_PROGRAM_ID), this.provider, config.coder ); this.authority = config.authority ?? this.wallet.publicKey; this.activeSubAccountId = config.activeSubAccountId ?? 0; this.skipLoadUsers = config.skipLoadUsers ?? false; this.txVersion = config.txVersion ?? this.getTxVersionForNewWallet(config.wallet); this.txParams = { computeUnits: config.txParams?.computeUnits ?? 600_000, computeUnitsPrice: config.txParams?.computeUnitsPrice ?? 0, }; this.txHandler = config?.txHandler ?? new TxHandler({ connection: this.connection, // @ts-ignore wallet: this.provider.wallet, confirmationOptions: this.opts, opts: { returnBlockHeightsWithSignedTxCallbackData: config.enableMetricsEvents, onSignedCb: this.handleSignedTransaction.bind(this), preSignedCb: this.handlePreSignedTransaction.bind(this), }, config: config.txHandlerConfig, }); 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' ); } if (config.authoritySubAccountMap && config.subAccountIds) { throw new Error( 'Can only pass one of authoritySubaccountMap or subAccountIds' ); } if (config.authoritySubAccountMap && config.includeDelegates) { throw new Error( 'Can only pass one of authoritySubaccountMap or includeDelegates' ); } this.authoritySubAccountMap = config.authoritySubAccountMap ? config.authoritySubAccountMap : config.subAccountIds ? new Map([[this.authority.toString(), config.subAccountIds]]) : new Map<string, number[]>(); 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, }; } else if (config.accountSubscription?.type === 'grpc') { this.userAccountSubscriptionConfig = { type: 'grpc', resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, grpcConfigs: config.accountSubscription?.grpcConfigs, }; this.userStatsAccountSubscriptionConfig = { type: 'grpc', grpcConfigs: config.accountSubscription?.grpcConfigs, resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, }; } else { this.userAccountSubscriptionConfig = { type: 'websocket', resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, commitment: config.accountSubscription?.commitment, programUserAccountSubscriber: config.accountSubscription?.programUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'websocket', resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, 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 (!this.marketLookupTable) { this.marketLookupTable = new PublicKey( configs[this.env].MARKET_LOOKUP_TABLE ); } this.marketLookupTables = config.marketLookupTables; if (!this.marketLookupTables) { this.marketLookupTables = configs[this.env].MARKET_LOOKUP_TABLES.map( (tableAddr) => new PublicKey(tableAddr) ); } const delistedMarketSetting = config.delistedMarketSetting || DelistedMarketSetting.Unsubscribe; const noMarketsAndOraclesSpecified = config.perpMarketIndexes === undefined && config.spotMarketIndexes === undefined && config.oracleInfos === undefined; if (config.accountSubscription?.type === 'polling') { this.accountSubscriber = new PollingDriftClientAccountSubscriber( this.program, config.accountSubscription.accountLoader, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], config.oracleInfos ?? [], noMarketsAndOraclesSpecified, delistedMarketSetting ); } else if (config.accountSubscription?.type === 'grpc') { this.accountSubscriber = new gprcDriftClientAccountSubscriber( config.accountSubscription.grpcConfigs, this.program, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], config.oracleInfos ?? [], noMarketsAndOraclesSpecified, delistedMarketSetting, { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, } ); } else { const accountSubscriberClass = config.accountSubscription?.driftClientAccountSubscriber ?? WebSocketDriftClientAccountSubscriber; this.accountSubscriber = new accountSubscriberClass( this.program, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], config.oracleInfos ?? [], noMarketsAndOraclesSpecified, delistedMarketSetting, { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, }, config.accountSubscription?.commitment as Commitment ); } this.eventEmitter = this.accountSubscriber.eventEmitter; this.metricsEventEmitter = new EventEmitter(); if (config.enableMetricsEvents) { this.enableMetricsEvents = true; } this.txSender = config.txSender ?? new RetryTxSender({ connection: this.connection, wallet: this.wallet, opts: this.opts, txHandler: this.txHandler, }); this.sbOnDemandProgramdId = configs[this.env].SB_ON_DEMAND_PID; } 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<boolean> { 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<boolean>[] { return [...this.users.values()].map((user) => user.subscribe()); } /** * Forces the accountSubscriber to fetch account updates from rpc */ public async fetchAccounts(): Promise<void> { 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<void> { 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<void>[] { return [...this.users.values()].map((user) => user.unsubscribe()); } statePublicKey?: PublicKey; public async getStatePublicKey(): Promise<PublicKey> { 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<StateAccount> { 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<PerpMarketAccount | undefined> { 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<SpotMarketAccount | undefined> { 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, oracleSource: OracleSource ): DataAndSlot<OraclePriceData> | undefined { return this.accountSubscriber.getOraclePriceDataAndSlot( getOracleId(oraclePublicKey, oracleSource) ); } public async getSerumV3FulfillmentConfig( serumMarket: PublicKey ): Promise<SerumV3FulfillmentConfigAccount> { const address = await getSerumFulfillmentConfigPublicKey( this.program.programId, serumMarket ); return (await this.program.account.serumV3FulfillmentConfig.fetch( address )) as SerumV3FulfillmentConfigAccount; } public async getSerumV3FulfillmentConfigs(): Promise< SerumV3FulfillmentConfigAccount[] > { const accounts = await this.program.account.serumV3FulfillmentConfig.all(); return accounts.map( (account) => account.account ) as SerumV3FulfillmentConfigAccount[]; } public async getPhoenixV1FulfillmentConfig( phoenixMarket: PublicKey ): Promise<PhoenixV1FulfillmentConfigAccount> { const address = await getPhoenixFulfillmentConfigPublicKey( this.program.programId, phoenixMarket ); return (await this.program.account.phoenixV1FulfillmentConfig.fetch( address )) as PhoenixV1FulfillmentConfigAccount; } public async getPhoenixV1FulfillmentConfigs(): Promise< PhoenixV1FulfillmentConfigAccount[] > { const accounts = await this.program.account.phoenixV1FulfillmentConfig.all(); return accounts.map( (account) => account.account ) as PhoenixV1FulfillmentConfigAccount[]; } public async getOpenbookV2FulfillmentConfig( openbookMarket: PublicKey ): Promise<OpenbookV2FulfillmentConfigAccount> { const address = getOpenbookV2FulfillmentConfigPublicKey( this.program.programId, openbookMarket ); return (await this.program.account.openbookV2FulfillmentConfig.fetch( address )) as OpenbookV2FulfillmentConfigAccount; } public async getOpenbookV2FulfillmentConfigs(): Promise< OpenbookV2FulfillmentConfigAccount[] > { const accounts = await this.program.account.openbookV2FulfillmentConfig.all(); return accounts.map( (account) => account.account ) as OpenbookV2FulfillmentConfigAccount[]; } /** @deprecated use fetchAllLookupTableAccounts() */ public async fetchMarketLookupTableAccount(): Promise<AddressLookupTableAccount> { 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; } public async fetchAllLookupTableAccounts(): Promise< AddressLookupTableAccount[] > { if (this.lookupTableAccounts) return this.lookupTableAccounts; if (!this.marketLookupTables) { console.log('Market lookup table address not set'); return; } const lookupTableAccountResults = await Promise.all( this.marketLookupTables.map((lookupTable) => this.connection.getAddressLookupTable(lookupTable) ) ); const lookupTableAccounts = lookupTableAccountResults.map( (result) => result.value ); this.lookupTableAccounts = lookupTableAccounts; return lookupTableAccounts; } private getTxVersionForNewWallet(newWallet: IWallet) { if (!newWallet?.supportedTransactionVersions) return 0; // Assume versioned txs supported if wallet doesn't have a supportedTransactionVersions property const walletSupportsVersionedTxns = newWallet.supportedTransactionVersions?.has(0) || (newWallet.supportedTransactionVersions?.size ?? 0) > 1; return walletSupportsVersionedTxns ? 0 : 'legacy'; } /** * 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<string, number[]> ): Promise<boolean> { 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.txHandler.updateWallet(newWallet); this.provider = newProvider; this.program = newProgram; this.authority = newWallet.publicKey; this.activeSubAccountId = activeSubAccountId; this.userStatsAccountPublicKey = undefined; this.includeDelegates = includeDelegates ?? false; this.txVersion = this.getTxVersionForNewWallet(this.wallet); 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<string, number[]>(); /* Reset user stats account */ if (this.userStats?.isSubscribed) { await this.userStats.unsubscribe(); } this.userStats = undefined; this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.getUserStatsAccountPublicKey(), accountSubscription: this.userStatsAccountSubscriptionConfig, }); const subscriptionPromises: Promise<any>[] = [this.userStats.subscribe()]; let success = true; if (this.isSubscribed) { const reSubscribeUsersPromise = async () => { await Promise.all(this.unsubscribeUsers()); this.users.clear(); success = await this.addAndSubscribeToUsers(); }; subscriptionPromises.push(reSubscribeUsersPromise()); } await Promise.all(subscriptionPromises); return success; } /** * Update the subscribed accounts to a given authority, while leaving the * connected wallet intact. This allows a user to emulate another user's * account on the UI and sign permissionless transactions with their own wallet. * @param emulateAuthority */ public async emulateAccount(emulateAuthority: PublicKey): Promise<boolean> { this.skipLoadUsers = false; // Update provider for txSender with new wallet details this.authority = emulateAuthority; this.userStatsAccountPublicKey = undefined; this.includeDelegates = true; this.txVersion = this.getTxVersionForNewWallet(this.wallet); this.authoritySubAccountMap = new Map<string, number[]>(); /* Reset user stats account */ if (this.userStats?.isSubscribed) { await this.userStats.unsubscribe(); } this.userStats = undefined; this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.getUserStatsAccountPublicKey(), accountSubscription: this.userStatsAccountSubscriptionConfig, }); await this.userStats.subscribe(); let success = true; if (this.isSubscribed) { await Promise.all(this.unsubscribeUsers()); this.users.clear(); success = await this.addAndSubscribeToUsers(emulateAuthority); } 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 && this.userStats) { if (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<boolean> { 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(authority?: PublicKey): Promise<boolean> { // 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 { let userAccounts = []; let delegatedAccounts = []; const userAccountsPromise = this.getUserAccountsForAuthority( authority ?? this.wallet.publicKey ); if (this.includeDelegates) { const delegatedAccountsPromise = this.getUserAccountsForDelegate( authority ?? this.wallet.publicKey ); [userAccounts, delegatedAccounts] = await Promise.all([ userAccountsPromise, delegatedAccountsPromise, ]); !userAccounts && (userAccounts = []); !delegatedAccounts && (delegatedAccounts = []); } else { userAccounts = (await userAccountsPromise) ?? []; } const allAccounts = userAccounts.concat(delegatedAccounts); const addAllAccountsPromise = allAccounts.map((acc) => this.addUser(acc.subAccountId, acc.authority, acc) ); const addAllAccountsResults = await Promise.all(addAllAccountsPromise); result = addAllAccountsResults.every((res) => !!res); if (this.activeSubAccountId == undefined) { this.switchActiveUser( userAccounts.concat(delegatedAccounts)[0]?.subAccountId ?? 0, userAccounts.concat(delegatedAccounts)[0]?.authority ?? this.authority ); } } return result; } /** * Returns the instructions to initialize a user account and the public key of the user account. * @param subAccountId * @param name * @param referrerInfo * @returns [instructions, userAccountPublicKey] */ public async getInitializeUserAccountIxs( subAccountId = 0, name?: string, referrerInfo?: ReferrerInfo, poolId?: number ): Promise<[TransactionInstruction[], PublicKey]> { const initializeIxs: TransactionInstruction[] = []; 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); if (poolId) { initializeIxs.push( await this.getUpdateUserPoolIdIx(poolId, subAccountId) ); } return [initializeIxs, userAccountPublicKey]; } /** * Initializes a user account and returns the transaction signature and the public key of the user account. * @param subAccountId * @param name * @param referrerInfo * @param txParams * @returns [transactionSignature, userAccountPublicKey] */ public async initializeUserAccount( subAccountId = 0, name?: string, referrerInfo?: ReferrerInfo, txParams?: TxParams ): Promise<[TransactionSignature, PublicKey]> { const [initializeIxs, userAccountPublicKey] = await this.getInitializeUserAccountIxs(subAccountId, name, referrerInfo); const tx = await this.buildTransaction(initializeIxs, txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); await this.addUser(subAccountId); return [txSig, userAccountPublicKey]; } async getInitializeUserStatsIx(): Promise<TransactionInstruction> { return await this.program.instruction.initializeUserStats({ accounts: { userStats: getUserStatsAccountPublicKey( this.program.programId, this.wallet.publicKey // only allow payer to initialize own user stats account ), 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 initializeSignedMsgUserOrders( authority: PublicKey, numOrders: number, txParams?: TxParams ): Promise<[TransactionSignature, PublicKey]> { const initializeIxs = []; const [signedMsgUserAccountPublicKey, initializeUserAccountIx] = await this.getInitializeSignedMsgUserOrdersAccountIx( authority, numOrders ); initializeIxs.push(initializeUserAccountIx); const tx = await this.buildTransaction(initializeIxs, txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return [txSig, signedMsgUserAccountPublicKey]; } async getInitializeSignedMsgUserOrdersAccountIx( authority: PublicKey, numOrders: number ): Promise<[PublicKey, TransactionInstruction]> { const signedMsgUserAccountPublicKey = getSignedMsgUserAccountPublicKey( this.program.programId, authority ); const initializeUserAccountIx = await this.program.instruction.initializeSignedMsgUserOrders(numOrders, { accounts: { signedMsgUserOrders: signedMsgUserAccountPublicKey, authority, payer: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, }); return [signedMsgUserAccountPublicKey, initializeUserAccountIx]; } public async resizeSignedMsgUserOrders( authority: PublicKey, numOrders: number, userSubaccountId?: number, txParams?: TxParams ): Promise<TransactionSignature> { const resizeUserAccountIx = await this.getResizeSignedMsgUserOrdersInstruction( authority, numOrders, userSubaccountId ); const tx = await this.buildTransaction([resizeUserAccountIx], txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } async getResizeSignedMsgUserOrdersInstruction( authority: PublicKey, numOrders: number, userSubaccountId?: number ): Promise<TransactionInstruction> { const signedMsgUserAccountPublicKey = getSignedMsgUserAccountPublicKey( this.program.programId, authority ); const resizeUserAccountIx = await this.program.instruction.resizeSignedMsgUserOrders(numOrders, { accounts: { signedMsgUserOrders: signedMsgUserAccountPublicKey, authority, payer: this.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, user: await getUserAccountPublicKey( this.program.programId, authority, userSubaccountId ), }, }); return resizeUserAccountIx; } public async initializeSignedMsgWsDelegatesAccount( authority: PublicKey, delegates: PublicKey[] = [], txParams?: TxParams ): Promise<TransactionSignature> { const ix = await this.getInitializeSignedMsgWsDelegatesAccountIx( authority, delegates ); const tx = await this.buildTransaction([ix], txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getInitializeSignedMsgWsDelegatesAccountIx( authority: PublicKey, delegates: PublicKey[] = [] ): Promise<TransactionInstruction> { const signedMsgWsDelegates = getSignedMsgWsDelegatesAccountPublicKey( this.program.programId, authority ); const ix = await this.program.instruction.initializeSignedMsgWsDelegates( delegates, { accounts: { signedMsgWsDelegates, authority: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, } ); return ix; } public async addSignedMsgWsDelegate( authority: PublicKey, delegate: PublicKey, txParams?: TxParams ): Promise<TransactionSignature> { const ix = await this.getAddSignedMsgWsDelegateIx(authority, delegate); const tx = await this.buildTransaction([ix], txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getAddSignedMsgWsDelegateIx( authority: PublicKey, delegate: PublicKey ): Promise<TransactionInstruction> { const signedMsgWsDelegates = getSignedMsgWsDelegatesAccountPublicKey( this.program.programId, authority ); const ix = await this.program.instruction.changeSignedMsgWsDelegateStatus( delegate, true, { accounts: { signedMsgWsDelegates, authority: this.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }, } ); return ix; } public async removeSignedMsgWsDelegate( authority: PublicKey, delegate: PublicKey, txParams?: TxParams ): Promise<TransactionSignature> { const ix = await this.getRemoveSignedMsgWsDelegateIx(authority, delegate); const tx = await this.buildTransaction([ix], txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getRemoveSignedMsgWsDelegateIx( authority: PublicKey, delegate: PublicKey ): Promise<TransactionInstruction> { const signedMsgWsDelegates = getSignedMsgWsDelegatesAccountPublicKey( this.program.programId, authority ); const ix = await this.program.instruction.changeSignedMsgWsDelegateStatus( delegate, false, { accounts: { signedMsgWsDelegates, authority: this.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }, } ); return ix; } public async initializeFuelOverflow( authority?: PublicKey ): Promise<TransactionSignature> { const ix = await this.getInitializeFuelOverflowIx(authority); const tx = await this.buildTransaction([ix], this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getInitializeFuelOverflowIx( authority?: PublicKey ): Promise<TransactionInstruction> { return await this.program.instruction.initializeFuelOverflow({ accounts: { fuelOverflow: getFuelOverflowAccountPublicKey( this.program.programId, authority ?? this.wallet.publicKey ), userStats: getUserStatsAccountPublicKey( this.program.programId, authority ?? this.wallet.publicKey ), authority: authority ?? this.wallet.publicKey, payer: this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, }); } public async sweepFuel(authority?: PublicKey): Promise<TransactionSignature> { const ix = await this.getSweepFuelIx(authority); const tx = await this.buildTransaction([ix], this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getSweepFuelIx( authority?: PublicKey ): Promise<TransactionInstruction> { return await this.program.instruction.sweepFuel({ accounts: { fuelOverflow: getFuelOverflowAccountPublicKey( this.program.programId, authority ?? this.wallet.publicKey ), userStats: getUserStatsAccountPublicKey( this.program.programId, authority ?? this.wallet.publicKey ), authority: authority ?? this.wallet.publicKey, signer: this.wallet.publicKey, }, }); } private 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<AccountMeta>(); 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 getNextSubAccountId(): Promise<number> { 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<TransactionSignature> { 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<TransactionSignature> { 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<TransactionSignature> { 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<TransactionInstruction> { 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 getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex: number, marginRatio: number, subAccountId = 0 ): Promise<TransactionInstruction> { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ); await this.addUser(subAccountId, this.wallet.publicKey); const ix = this.program.instruction.updateUserPerpPositionCustomMarginRatio( subAccountId, perpMarketIndex, marginRatio, { accounts: { user: userAccountPublicKey, authority: this.wallet.publicKey, }, } ); return ix; } public async updateUserPerpPositionCustomMarginRatio( perpMarketIndex: number, marginRatio: number, subAccountId = 0, txParams?: TxParams ): Promise<TransactionSignature> { const ix = await this.getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex, marginRatio, subAccountId ); const tx = await this.buildTransaction(ix, txParams ?? this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getUpdateUserMarginTradingEnabledIx( marginTradingEnabled: boolean, subAccountId = 0, userAccountPublicKey?: PublicKey ): Promise<TransactionInstruction> { 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<TransactionSignature> { 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 getUpdateUserDelegateIx( delegate: PublicKey, overrides: { subAccountId?: number; userAccountPublicKey?: PublicKey; authority?: PublicKey; } ): Promise<TransactionInstruction> { const subAccountId = overrides.subAccountId ?? this.activeSubAccountId; const userAccountPublicKey = overrides.userAccountPublicKey ?? (await this.getUserAccountPublicKey()); const authority = overrides.authority ?? this.wallet.publicKey; return await this.program.instruction.updateUserDelegate( subAccountId, delegate, { accounts: { user: userAccountPublicKey, authority, }, } ); } public async updateUserDelegate( delegate: PublicKey, subAccountId = 0 ): Promise<TransactionSignature> { 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 updateUserAdvancedLp( updates: { advancedLp: boolean; subAccountId: number }[] ): Promise<TransactionSignature> { const ixs = await Promise.all( updates.map(async ({ advancedLp, subAccountId }) => { return await this.getUpdateAdvancedDlpIx(advancedLp, subAccountId); }) ); const tx = await this.buildTransaction(ixs, this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } public async getUpdateAdvancedDlpIx( advancedLp: boolean, subAccountId: number ) { const ix = await this.program.instruction.updateUserAdvancedLp( subAccountId, advancedLp, { accounts: { user: getUserAccountPublicKeySync( this.program.programId, this.wallet.publicKey, subAccountId ), authority: this.wallet.publicKey, }, } ); return ix; } public async updateUserReduceOnly( updates: { reduceOnly: boolean; subAccountId: number }[] ): Promise<TransactionSignature> { const ixs = await Promise.all( updates.map(async ({ reduceOnly, subAccountId }) => { return await this.getUpdateUserReduceOnlyIx(reduceOnly, subAccountId); }) ); const tx = await this.buildTransaction(ixs, this.txParams); const { txSig } = await this.s