diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c618fb8..836bd819c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: reusue unused maker order id as success condition for place and take perp order ([#1218](https://github.com/drift-labs/protocol-v2/pull/1218)) - program/sdk: swift for devnet ([#1195](https://github.com/drift-labs/protocol-v2/pull/1195)) - sdk: EventSubscriber: support events server ([#1222](https://github.com/drift-labs/protocol-v2/pull/1222)) +- sdk: add new DelistMarketSetting to handle delisted markets ([#1229](https://github.com/drift-labs/protocol-v2/pull/1229)) ### Fixes diff --git a/sdk/src/accounts/pollingDriftClientAccountSubscriber.ts b/sdk/src/accounts/pollingDriftClientAccountSubscriber.ts index b35a3137e..de96b3927 100644 --- a/sdk/src/accounts/pollingDriftClientAccountSubscriber.ts +++ b/sdk/src/accounts/pollingDriftClientAccountSubscriber.ts @@ -1,6 +1,7 @@ import { - DataAndSlot, AccountToPoll, + DataAndSlot, + DelistedMarketSetting, DriftClientAccountEvents, DriftClientAccountSubscriber, NotSubscribedError, @@ -10,18 +11,18 @@ import { Program } from '@coral-xyz/anchor'; import StrictEventEmitter from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; import { - SpotMarketAccount, PerpMarketAccount, + SpotMarketAccount, StateAccount, UserAccount, } from '../types'; import { getDriftStateAccountPublicKey, - getSpotMarketPublicKey, getPerpMarketPublicKey, + getSpotMarketPublicKey, } from '../addresses/pda'; import { BulkAccountLoader } from './bulkAccountLoader'; -import { capitalize } from './utils'; +import { capitalize, findDelistedPerpMarketsAndOracles } from './utils'; import { PublicKey } from '@solana/web3.js'; import { OracleInfo, OraclePriceData } from '../oracles/types'; import { OracleClientCache } from '../oracles/oracleClientCache'; @@ -58,6 +59,7 @@ export class PollingDriftClientAccountSubscriber spotOracleStringMap = new Map(); oracles = new Map>(); user?: DataAndSlot; + delistedMarketSetting: DelistedMarketSetting; private isSubscribing = false; private subscriptionPromise: Promise; @@ -69,7 +71,8 @@ export class PollingDriftClientAccountSubscriber perpMarketIndexes: number[], spotMarketIndexes: number[], oracleInfos: OracleInfo[], - shouldFindAllMarketsAndOracles: boolean + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting ) { this.isSubscribed = false; this.program = program; @@ -79,6 +82,7 @@ export class PollingDriftClientAccountSubscriber this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; + this.delistedMarketSetting = delistedMarketSetting; } public async subscribe(): Promise { @@ -120,6 +124,8 @@ export class PollingDriftClientAccountSubscriber this.eventEmitter.emit('update'); } + this.handleDelistedMarkets(); + await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); this.isSubscribing = false; @@ -500,6 +506,36 @@ export class PollingDriftClientAccountSubscriber await Promise.all(oraclePromises); } + handleDelistedMarkets(): void { + if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) { + return; + } + + const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles( + this.getMarketAccountsAndSlots(), + this.getSpotMarketAccountsAndSlots() + ); + + for (const perpMarketIndex of perpMarketIndexes) { + const perpMarketPubkey = this.perpMarket.get(perpMarketIndex).data.pubkey; + const callbackId = this.accountsToPoll.get( + perpMarketPubkey.toBase58() + ).callbackId; + this.accountLoader.removeAccount(perpMarketPubkey, callbackId); + if (this.delistedMarketSetting === DelistedMarketSetting.Discard) { + this.perpMarket.delete(perpMarketIndex); + } + } + + for (const oracle of oracles) { + const callbackId = this.oraclesToPoll.get(oracle.toBase58()).callbackId; + this.accountLoader.removeAccount(oracle, callbackId); + if (this.delistedMarketSetting === DelistedMarketSetting.Discard) { + this.oracles.delete(oracle.toBase58()); + } + } + } + assertIsSubscribed(): void { if (!this.isSubscribed) { throw new NotSubscribedError( diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 5b0066f29..b6c45f0fe 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -84,6 +84,12 @@ export interface DriftClientAccountSubscriber { updateAccountLoaderPollingFrequency?: (pollingFrequency: number) => void; } +export enum DelistedMarketSetting { + Unsubscribe, + Subscribe, + Discard, +} + export interface UserAccountEvents { userAccountUpdate: (payload: UserAccount) => void; update: void; diff --git a/sdk/src/accounts/utils.ts b/sdk/src/accounts/utils.ts index 90c731c65..b0c57bb94 100644 --- a/sdk/src/accounts/utils.ts +++ b/sdk/src/accounts/utils.ts @@ -1,3 +1,45 @@ +import { PublicKey } from '@solana/web3.js'; +import { DataAndSlot } from './types'; +import { isVariant, PerpMarketAccount, SpotMarketAccount } from '../types'; + export function capitalize(value: string): string { return value[0].toUpperCase() + value.slice(1); } + +export function findDelistedPerpMarketsAndOracles( + perpMarkets: DataAndSlot[], + spotMarkets: DataAndSlot[] +): { perpMarketIndexes: number[]; oracles: PublicKey[] } { + const delistedPerpMarketIndexes = []; + const delistedOracles = []; + for (const perpMarket of perpMarkets) { + if (!perpMarket.data) { + continue; + } + + if (isVariant(perpMarket.data.status, 'delisted')) { + delistedPerpMarketIndexes.push(perpMarket.data.marketIndex); + delistedOracles.push(perpMarket.data.amm.oracle); + } + } + + // make sure oracle isn't used by spot market + const filteredDelistedOracles = []; + for (const delistedOracle of delistedOracles) { + for (const spotMarket of spotMarkets) { + if (!spotMarket.data) { + continue; + } + + if (spotMarket.data.oracle.equals(delistedOracle)) { + break; + } + } + filteredDelistedOracles.push(delistedOracle); + } + + return { + perpMarketIndexes: delistedPerpMarketIndexes, + oracles: filteredDelistedOracles, + }; +} diff --git a/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts b/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts index dc410cb98..7048b93ab 100644 --- a/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts +++ b/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts @@ -1,19 +1,21 @@ import { - DriftClientAccountSubscriber, - DriftClientAccountEvents, + AccountSubscriber, DataAndSlot, + DelistedMarketSetting, + DriftClientAccountEvents, + DriftClientAccountSubscriber, + NotSubscribedError, ResubOpts, } from './types'; -import { AccountSubscriber, NotSubscribedError } from './types'; -import { SpotMarketAccount, PerpMarketAccount, StateAccount } from '../types'; +import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; import { Program } from '@coral-xyz/anchor'; import StrictEventEmitter from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; import { getDriftStateAccountPublicKey, - getSpotMarketPublicKey, getPerpMarketPublicKey, getPerpMarketPublicKeySync, + getSpotMarketPublicKey, getSpotMarketPublicKeySync, } from '../addresses/pda'; import { WebSocketAccountSubscriber } from './webSocketAccountSubscriber'; @@ -23,6 +25,7 @@ import { OracleClientCache } from '../oracles/oracleClientCache'; import * as Buffer from 'buffer'; import { QUOTE_ORACLE_PRICE_DATA } from '../oracles/quoteAssetOracleClient'; import { findAllMarketAndOracles } from '../config'; +import { findDelistedPerpMarketsAndOracles } from './utils'; const ORACLE_DEFAULT_KEY = PublicKey.default.toBase58(); @@ -55,6 +58,7 @@ export class WebSocketDriftClientAccountSubscriber spotOracleMap = new Map(); spotOracleStringMap = new Map(); oracleSubscribers = new Map>(); + delistedMarketSetting: DelistedMarketSetting; initialPerpMarketAccountData: Map; initialSpotMarketAccountData: Map; @@ -70,6 +74,7 @@ export class WebSocketDriftClientAccountSubscriber spotMarketIndexes: number[], oracleInfos: OracleInfo[], shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting, resubOpts?: ResubOpts, commitment?: Commitment ) { @@ -80,6 +85,7 @@ export class WebSocketDriftClientAccountSubscriber this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; + this.delistedMarketSetting = delistedMarketSetting; this.resubOpts = resubOpts; this.commitment = commitment; } @@ -151,6 +157,8 @@ export class WebSocketDriftClientAccountSubscriber this.eventEmitter.emit('update'); + await this.handleDelistedMarkets(); + await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); this.isSubscribing = false; @@ -480,6 +488,33 @@ export class WebSocketDriftClientAccountSubscriber await Promise.all(addOraclePromises); } + async handleDelistedMarkets(): Promise { + if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) { + return; + } + + const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles( + this.getMarketAccountsAndSlots(), + this.getSpotMarketAccountsAndSlots() + ); + + for (const perpMarketIndex of perpMarketIndexes) { + await this.perpMarketAccountSubscribers + .get(perpMarketIndex) + .unsubscribe(); + if (this.delistedMarketSetting === DelistedMarketSetting.Discard) { + this.perpMarketAccountSubscribers.delete(perpMarketIndex); + } + } + + for (const oracle of oracles) { + await this.oracleSubscribers.get(oracle.toBase58()).unsubscribe(); + if (this.delistedMarketSetting === DelistedMarketSetting.Discard) { + this.oracleSubscribers.delete(oracle.toBase58()); + } + } + } + assertIsSubscribed(): void { if (!this.isSubscribed) { throw new NotSubscribedError( diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 8060a6a27..f6988566e 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1,3 +1,4 @@ +import * as anchor from '@coral-xyz/anchor'; import { AnchorProvider, BN, @@ -13,65 +14,65 @@ import { createCloseAccountInstruction, createInitializeAccountInstruction, getAssociatedTokenAddress, - TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, } from '@solana/spl-token'; import { - StateAccount, + DriftClientMetricsEvents, + isVariant, IWallet, - PositionDirection, - UserAccount, - PerpMarketAccount, - OrderParams, - Order, - SpotMarketAccount, - SpotPosition, MakerInfo, - TakerInfo, - OptionalOrderParams, - OrderType, - ReferrerInfo, + MappedRecord, MarketType, - TxParams, - SerumV3FulfillmentConfigAccount, - isVariant, - ReferrerNameAccount, + ModifyOrderParams, + ModifyOrderPolicy, + OpenbookV2FulfillmentConfigAccount, + OptionalOrderParams, + Order, + OrderParams, OrderTriggerCondition, - SpotBalanceType, + OrderType, + PerpMarketAccount, PerpMarketExtendedInfo, - UserStatsAccount, - ModifyOrderParams, PhoenixV1FulfillmentConfigAccount, - ModifyOrderPolicy, - SwapReduceOnly, + PlaceAndTakeOrderSuccessCondition, + PositionDirection, + ReferrerInfo, + ReferrerNameAccount, + SerumV3FulfillmentConfigAccount, SettlePnlMode, SignedTxData, - MappedRecord, - OpenbookV2FulfillmentConfigAccount, - PlaceAndTakeOrderSuccessCondition, + SpotBalanceType, + SpotMarketAccount, + SpotPosition, + StateAccount, + SwapReduceOnly, SwiftOrderParamsMessage, + TakerInfo, + TxParams, + UserAccount, + UserStatsAccount, } from './types'; -import * as anchor from '@coral-xyz/anchor'; import driftIDL from './idl/drift.json'; import { - Connection, - PublicKey, - TransactionSignature, - ConfirmOptions, - Transaction, - TransactionInstruction, AccountMeta, + AddressLookupTableAccount, + BlockhashWithExpiryBlockHeight, + ConfirmOptions, + Connection, + Ed25519Program, Keypair, LAMPORTS_PER_SOL, + PublicKey, Signer, SystemProgram, - AddressLookupTableAccount, + SYSVAR_INSTRUCTIONS_PUBKEY, + Transaction, + TransactionInstruction, + TransactionSignature, TransactionVersion, VersionedTransaction, - BlockhashWithExpiryBlockHeight, - SYSVAR_INSTRUCTIONS_PUBKEY, - Ed25519Program, } from '@solana/web3.js'; import { TokenFaucet } from './tokenFaucet'; @@ -94,19 +95,19 @@ import { getUserStatsAccountPublicKey, } from './addresses/pda'; import { - DriftClientAccountSubscriber, - DriftClientAccountEvents, DataAndSlot, + DelistedMarketSetting, + DriftClientAccountEvents, + DriftClientAccountSubscriber, } from './accounts/types'; -import { DriftClientMetricsEvents } from './types'; import { TxSender, TxSigAndSlot } from './tx/types'; import { BASE_PRECISION, + GOV_SPOT_MARKET_INDEX, PRICE_PRECISION, + QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, ZERO, - QUOTE_PRECISION, - GOV_SPOT_MARKET_INDEX, } from './constants/numericConstants'; import { findDirectionToClose, positionIsAvailable } from './math/position'; import { getSignedTokenAmount, getTokenAmount } from './math/spotBalance'; @@ -118,7 +119,7 @@ import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDrift import { RetryTxSender } from './tx/retryTxSender'; import { User } from './user'; import { UserSubscriptionConfig } from './userConfig'; -import { configs, DEFAULT_CONFIRMATION_OPTS, DRIFT_PROGRAM_ID } from './config'; +import { configs, DRIFT_ORACLE_RECEIVER_ID, DEFAULT_CONFIRMATION_OPTS, DRIFT_PROGRAM_ID } from './config'; import { WRAPPED_SOL_MINT } from './constants/spotMarkets'; import { UserStats } from './userStats'; import { isSpotPositionAvailable } from './math/spotPosition'; @@ -140,15 +141,14 @@ import { TransactionParamProcessor } from './tx/txParamProcessor'; import { isOracleValid, trimVaaSignatures } from './math/oracles'; import { TxHandler } from './tx/txHandler'; import { - wormholeCoreBridgeIdl, 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 { DRIFT_ORACLE_RECEIVER_ID } from './config'; 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/pythPullOracleUtils'; @@ -331,6 +331,8 @@ export class DriftClient { ); } + const delistedMarketSetting = + config.delistedMarketSetting || DelistedMarketSetting.Subscribe; const noMarketsAndOraclesSpecified = config.perpMarketIndexes === undefined && config.spotMarketIndexes === undefined && @@ -342,7 +344,8 @@ export class DriftClient { config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], config.oracleInfos ?? [], - noMarketsAndOraclesSpecified + noMarketsAndOraclesSpecified, + delistedMarketSetting ); } else { this.accountSubscriber = new WebSocketDriftClientAccountSubscriber( @@ -351,6 +354,7 @@ export class DriftClient { config.spotMarketIndexes ?? [], config.oracleInfos ?? [], noMarketsAndOraclesSpecified, + delistedMarketSetting, { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index a95b10da4..97be1a733 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -11,6 +11,7 @@ import { BulkAccountLoader } from './accounts/bulkAccountLoader'; import { DriftEnv } from './config'; import { TxSender } from './tx/types'; import { TxHandler, TxHandlerConfig } from './tx/txHandler'; +import { DelistedMarketSetting } from './accounts/types'; export type DriftClientConfig = { connection: Connection; @@ -36,6 +37,7 @@ export type DriftClientConfig = { txParams?: TxParams; // default tx params to use enableMetricsEvents?: boolean; txHandlerConfig?: TxHandlerConfig; + delistedMarketSetting?: DelistedMarketSetting; }; export type DriftClientSubscriptionConfig =