@drift-labs/sdk 2.96.0-beta.9 → 2.97.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/VERSION +1 -1
- package/bun.lockb +0 -0
- package/lib/accounts/pollingDriftClientAccountSubscriber.d.ts +5 -3
- package/lib/accounts/pollingDriftClientAccountSubscriber.js +24 -1
- package/lib/accounts/types.d.ts +5 -0
- package/lib/accounts/types.js +7 -1
- package/lib/accounts/utils.d.ts +7 -0
- package/lib/accounts/utils.js +33 -1
- package/lib/accounts/webSocketDriftClientAccountSubscriber.d.ts +5 -4
- package/lib/accounts/webSocketDriftClientAccountSubscriber.js +24 -1
- package/lib/config.d.ts +6 -1
- package/lib/config.js +10 -1
- package/lib/constants/perpMarkets.js +33 -1
- package/lib/constants/spotMarkets.js +10 -0
- package/lib/constants/txConstants.d.ts +1 -0
- package/lib/constants/txConstants.js +4 -0
- package/lib/driftClient.d.ts +36 -8
- package/lib/driftClient.js +166 -43
- package/lib/driftClientConfig.d.ts +3 -0
- package/lib/events/types.js +1 -5
- package/lib/idl/drift.json +169 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/orderParams.js +8 -8
- package/lib/orderSubscriber/OrderSubscriber.js +1 -6
- package/lib/tokenFaucet.js +2 -1
- package/lib/tx/baseTxSender.d.ts +0 -1
- package/lib/tx/baseTxSender.js +8 -26
- package/lib/tx/fastSingleTxSender.js +2 -2
- package/lib/tx/forwardOnlyTxSender.js +2 -2
- package/lib/tx/reportTransactionError.d.ts +20 -0
- package/lib/tx/reportTransactionError.js +103 -0
- package/lib/tx/retryTxSender.js +2 -2
- package/lib/tx/txHandler.js +10 -7
- package/lib/tx/whileValidTxSender.d.ts +4 -5
- package/lib/tx/whileValidTxSender.js +16 -17
- package/lib/types.d.ts +22 -1
- package/lib/types.js +6 -1
- package/lib/util/TransactionConfirmationManager.d.ts +16 -0
- package/lib/util/TransactionConfirmationManager.js +174 -0
- package/package.json +4 -3
- package/src/accounts/pollingDriftClientAccountSubscriber.ts +41 -5
- package/src/accounts/types.ts +6 -0
- package/src/accounts/utils.ts +42 -0
- package/src/accounts/webSocketDriftClientAccountSubscriber.ts +40 -5
- package/src/config.ts +17 -1
- package/src/constants/perpMarkets.ts +35 -1
- package/src/constants/spotMarkets.ts +11 -0
- package/src/constants/txConstants.ts +1 -0
- package/src/driftClient.ts +346 -53
- package/src/driftClientConfig.ts +3 -0
- package/src/events/types.ts +1 -5
- package/src/idl/drift.json +169 -1
- package/src/index.ts +1 -0
- package/src/orderParams.ts +20 -12
- package/src/orderSubscriber/OrderSubscriber.ts +2 -5
- package/src/tokenFaucet.ts +2 -2
- package/src/tx/baseTxSender.ts +10 -32
- package/src/tx/fastSingleTxSender.ts +2 -2
- package/src/tx/forwardOnlyTxSender.ts +2 -2
- package/src/tx/reportTransactionError.ts +159 -0
- package/src/tx/retryTxSender.ts +2 -2
- package/src/tx/txHandler.ts +8 -2
- package/src/tx/whileValidTxSender.ts +18 -27
- package/src/types.ts +31 -1
- package/src/util/TransactionConfirmationManager.ts +292 -0
- package/tests/ci/verifyConstants.ts +13 -0
- package/tests/tx/TransactionConfirmationManager.test.ts +305 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TransactionConfirmationManager = void 0;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const __1 = require("..");
|
|
6
|
+
const txConstants_1 = require("../constants/txConstants");
|
|
7
|
+
const reportTransactionError_1 = require("../tx/reportTransactionError");
|
|
8
|
+
const promiseTimeout_1 = require("./promiseTimeout");
|
|
9
|
+
const confirmationStatusValues = {
|
|
10
|
+
processed: 0,
|
|
11
|
+
confirmed: 1,
|
|
12
|
+
finalized: 2,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Class to await for transaction confirmations in an optimised manner. It tracks a shared list of all pending transactions and fetches them in bulk in a shared RPC request whenever they have an "overlapping" polling interval. E.g. tx1 with an interval of 200ms and tx2 with an interval of 300ms (if sent at the same time) will be fetched together at at 600ms, 1200ms, 1800ms, etc.
|
|
16
|
+
*/
|
|
17
|
+
class TransactionConfirmationManager {
|
|
18
|
+
constructor(connection) {
|
|
19
|
+
this.pendingConfirmations = new Map();
|
|
20
|
+
this.intervalId = null;
|
|
21
|
+
this.connection = connection;
|
|
22
|
+
}
|
|
23
|
+
async confirmTransactionWebSocket(txSig, timeout = 30000, desiredConfirmationStatus = config_1.DEFAULT_CONFIRMATION_OPTS.commitment) {
|
|
24
|
+
const start = Date.now();
|
|
25
|
+
const subscriptionCommitment = desiredConfirmationStatus || config_1.DEFAULT_CONFIRMATION_OPTS.commitment;
|
|
26
|
+
let response = null;
|
|
27
|
+
let subscriptionId;
|
|
28
|
+
const confirmationPromise = new Promise((resolve, reject) => {
|
|
29
|
+
try {
|
|
30
|
+
subscriptionId = this.connection.onSignature(txSig, (result, context) => {
|
|
31
|
+
response = {
|
|
32
|
+
context,
|
|
33
|
+
value: result,
|
|
34
|
+
};
|
|
35
|
+
resolve(null);
|
|
36
|
+
}, subscriptionCommitment);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
reject(err);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// We do a one-shot confirmation check just in case the transaction is ALREADY confirmed when we create the websocket confirmation .. We want to run this concurrently with the onSignature subscription. If this returns true then we can return early as the transaction has already been confirmed.
|
|
43
|
+
const oneShotConfirmationPromise = this.connection.getSignatureStatuses([
|
|
44
|
+
txSig,
|
|
45
|
+
]);
|
|
46
|
+
const resolveReference = {};
|
|
47
|
+
// This is the promise we are waiting on to resolve the overall confirmation. It will resolve the faster of a positive oneShot confirmation, or the websocket confirmation, or the timeout.
|
|
48
|
+
const overallWaitingForConfirmationPromise = new Promise((resolve) => {
|
|
49
|
+
resolveReference.resolve = resolve;
|
|
50
|
+
});
|
|
51
|
+
// Await for the one shot confirmation and resolve the waiting promise if we get a positive confirmation result
|
|
52
|
+
oneShotConfirmationPromise.then(async (oneShotResponse) => {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
if (!oneShotResponse || !((_a = oneShotResponse === null || oneShotResponse === void 0 ? void 0 : oneShotResponse.value) === null || _a === void 0 ? void 0 : _a[0]))
|
|
55
|
+
return;
|
|
56
|
+
const resultValue = oneShotResponse.value[0];
|
|
57
|
+
if (resultValue.err) {
|
|
58
|
+
await (0, reportTransactionError_1.throwTransactionError)(txSig, this.connection);
|
|
59
|
+
}
|
|
60
|
+
if (this.checkStatusMatchesDesiredConfirmationStatus(resultValue, desiredConfirmationStatus)) {
|
|
61
|
+
response = {
|
|
62
|
+
context: oneShotResponse.context,
|
|
63
|
+
value: oneShotResponse.value[0],
|
|
64
|
+
};
|
|
65
|
+
(_b = resolveReference.resolve) === null || _b === void 0 ? void 0 : _b.call(resolveReference);
|
|
66
|
+
}
|
|
67
|
+
}, (onRejected) => {
|
|
68
|
+
throw onRejected;
|
|
69
|
+
});
|
|
70
|
+
// Await for the websocket confirmation with the configured timeout
|
|
71
|
+
(0, promiseTimeout_1.promiseTimeout)(confirmationPromise, timeout).then(() => {
|
|
72
|
+
var _a;
|
|
73
|
+
(_a = resolveReference.resolve) === null || _a === void 0 ? void 0 : _a.call(resolveReference);
|
|
74
|
+
}, (onRejected) => {
|
|
75
|
+
throw onRejected;
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await overallWaitingForConfirmationPromise;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (subscriptionId !== undefined) {
|
|
82
|
+
this.connection.removeSignatureListener(subscriptionId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const duration = (Date.now() - start) / 1000;
|
|
86
|
+
if (response === null) {
|
|
87
|
+
throw new __1.TxSendError(`Transaction was not confirmed in ${duration.toFixed(2)} seconds. It is unknown if it succeeded or failed. Check signature ${txSig} using the Solana Explorer or CLI tools.`, txConstants_1.NOT_CONFIRMED_ERROR_CODE);
|
|
88
|
+
}
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
async confirmTransactionPolling(txSig, desiredConfirmationStatus = config_1.DEFAULT_CONFIRMATION_OPTS.commitment, timeout = 30000, pollInterval = 1000, searchTransactionHistory = false) {
|
|
92
|
+
// Interval must be > 400ms and a multiple of 100ms
|
|
93
|
+
if (pollInterval < 400 || pollInterval % 100 !== 0) {
|
|
94
|
+
throw new Error('Transaction confirmation polling interval must be at least 400ms and a multiple of 100ms');
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
this.pendingConfirmations.set(txSig, {
|
|
98
|
+
txSig,
|
|
99
|
+
desiredConfirmationStatus,
|
|
100
|
+
timeout,
|
|
101
|
+
pollInterval,
|
|
102
|
+
searchTransactionHistory,
|
|
103
|
+
startTime: Date.now(),
|
|
104
|
+
resolve,
|
|
105
|
+
reject,
|
|
106
|
+
});
|
|
107
|
+
if (!this.intervalId) {
|
|
108
|
+
this.startConfirmationLoop();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
startConfirmationLoop() {
|
|
113
|
+
this.intervalId = setInterval(() => this.checkPendingConfirmations(), 100);
|
|
114
|
+
}
|
|
115
|
+
async checkPendingConfirmations() {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const transactionsToCheck = [];
|
|
118
|
+
for (const [txSig, request] of this.pendingConfirmations.entries()) {
|
|
119
|
+
if (now - request.startTime >= request.timeout) {
|
|
120
|
+
request.reject(new Error(`Transaction confirmation timeout after ${request.timeout}ms`));
|
|
121
|
+
this.pendingConfirmations.delete(txSig);
|
|
122
|
+
}
|
|
123
|
+
else if ((now - request.startTime) % request.pollInterval < 100) {
|
|
124
|
+
transactionsToCheck.push(request);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (transactionsToCheck.length > 0) {
|
|
128
|
+
await this.checkTransactionStatuses(transactionsToCheck);
|
|
129
|
+
}
|
|
130
|
+
if (this.pendingConfirmations.size === 0 && this.intervalId) {
|
|
131
|
+
clearInterval(this.intervalId);
|
|
132
|
+
this.intervalId = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
checkStatusMatchesDesiredConfirmationStatus(status, desiredConfirmationStatus) {
|
|
136
|
+
if (status.confirmationStatus &&
|
|
137
|
+
confirmationStatusValues[status.confirmationStatus] >=
|
|
138
|
+
confirmationStatusValues[desiredConfirmationStatus]) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
async checkTransactionStatuses(requests) {
|
|
144
|
+
const txSigs = requests.map((request) => request.txSig);
|
|
145
|
+
const { value: statuses } = await this.connection.getSignatureStatuses(txSigs, {
|
|
146
|
+
searchTransactionHistory: requests.some((req) => req.searchTransactionHistory),
|
|
147
|
+
});
|
|
148
|
+
if (!statuses || statuses.length !== txSigs.length) {
|
|
149
|
+
throw new Error('Failed to get signature statuses');
|
|
150
|
+
}
|
|
151
|
+
for (let i = 0; i < statuses.length; i++) {
|
|
152
|
+
const status = statuses[i];
|
|
153
|
+
const request = requests[i];
|
|
154
|
+
if (status === null) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (status.err) {
|
|
158
|
+
this.pendingConfirmations.delete(request.txSig);
|
|
159
|
+
request.reject(await (0, reportTransactionError_1.getTransactionErrorFromTxSig)(request.txSig, this.connection));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (confirmationStatusValues[status.confirmationStatus] === undefined ||
|
|
163
|
+
confirmationStatusValues[request.desiredConfirmationStatus] ===
|
|
164
|
+
undefined) {
|
|
165
|
+
throw new Error(`Invalid confirmation status when awaiting confirmation: ${status.confirmationStatus}`);
|
|
166
|
+
}
|
|
167
|
+
if (this.checkStatusMatchesDesiredConfirmationStatus(status, request.desiredConfirmationStatus)) {
|
|
168
|
+
request.resolve(status);
|
|
169
|
+
this.pendingConfirmations.delete(request.txSig);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.TransactionConfirmationManager = TransactionConfirmationManager;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drift-labs/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.97.0-beta.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"types": "lib/index.d.ts",
|
|
6
6
|
"author": "crispheaney",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@coral-xyz/anchor": "0.28.0",
|
|
38
38
|
"@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1",
|
|
39
39
|
"@ellipsis-labs/phoenix-sdk": "^1.4.2",
|
|
40
|
-
"@openbook-dex/openbook-v2": "
|
|
40
|
+
"@openbook-dex/openbook-v2": "0.2.10",
|
|
41
41
|
"@project-serum/serum": "^0.13.38",
|
|
42
42
|
"@pythnetwork/client": "2.5.3",
|
|
43
43
|
"@pythnetwork/price-service-sdk": "^1.7.1",
|
|
44
44
|
"@pythnetwork/pyth-solana-receiver": "^0.7.0",
|
|
45
45
|
"@solana/spl-token": "0.3.7",
|
|
46
46
|
"@solana/web3.js": "1.92.3",
|
|
47
|
-
"@switchboard-xyz/on-demand": "1.2.
|
|
47
|
+
"@switchboard-xyz/on-demand": "1.2.42",
|
|
48
48
|
"anchor-bankrun": "^0.3.0",
|
|
49
49
|
"node-cache": "^5.1.2",
|
|
50
50
|
"rpc-websockets": "7.5.1",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/big.js": "^6.2.0",
|
|
58
58
|
"@types/bn.js": "^5.1.3",
|
|
59
|
+
"@types/bs58": "^4.0.4",
|
|
59
60
|
"@types/chai": "^4.3.1",
|
|
60
61
|
"@types/jest": "^28.1.3",
|
|
61
62
|
"@types/mocha": "^9.1.1",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DataAndSlot,
|
|
3
2
|
AccountToPoll,
|
|
3
|
+
DataAndSlot,
|
|
4
|
+
DelistedMarketSetting,
|
|
4
5
|
DriftClientAccountEvents,
|
|
5
6
|
DriftClientAccountSubscriber,
|
|
6
7
|
NotSubscribedError,
|
|
@@ -10,18 +11,18 @@ import { Program } from '@coral-xyz/anchor';
|
|
|
10
11
|
import StrictEventEmitter from 'strict-event-emitter-types';
|
|
11
12
|
import { EventEmitter } from 'events';
|
|
12
13
|
import {
|
|
13
|
-
SpotMarketAccount,
|
|
14
14
|
PerpMarketAccount,
|
|
15
|
+
SpotMarketAccount,
|
|
15
16
|
StateAccount,
|
|
16
17
|
UserAccount,
|
|
17
18
|
} from '../types';
|
|
18
19
|
import {
|
|
19
20
|
getDriftStateAccountPublicKey,
|
|
20
|
-
getSpotMarketPublicKey,
|
|
21
21
|
getPerpMarketPublicKey,
|
|
22
|
+
getSpotMarketPublicKey,
|
|
22
23
|
} from '../addresses/pda';
|
|
23
24
|
import { BulkAccountLoader } from './bulkAccountLoader';
|
|
24
|
-
import { capitalize } from './utils';
|
|
25
|
+
import { capitalize, findDelistedPerpMarketsAndOracles } from './utils';
|
|
25
26
|
import { PublicKey } from '@solana/web3.js';
|
|
26
27
|
import { OracleInfo, OraclePriceData } from '../oracles/types';
|
|
27
28
|
import { OracleClientCache } from '../oracles/oracleClientCache';
|
|
@@ -58,6 +59,7 @@ export class PollingDriftClientAccountSubscriber
|
|
|
58
59
|
spotOracleStringMap = new Map<number, string>();
|
|
59
60
|
oracles = new Map<string, DataAndSlot<OraclePriceData>>();
|
|
60
61
|
user?: DataAndSlot<UserAccount>;
|
|
62
|
+
delistedMarketSetting: DelistedMarketSetting;
|
|
61
63
|
|
|
62
64
|
private isSubscribing = false;
|
|
63
65
|
private subscriptionPromise: Promise<boolean>;
|
|
@@ -69,7 +71,8 @@ export class PollingDriftClientAccountSubscriber
|
|
|
69
71
|
perpMarketIndexes: number[],
|
|
70
72
|
spotMarketIndexes: number[],
|
|
71
73
|
oracleInfos: OracleInfo[],
|
|
72
|
-
shouldFindAllMarketsAndOracles: boolean
|
|
74
|
+
shouldFindAllMarketsAndOracles: boolean,
|
|
75
|
+
delistedMarketSetting: DelistedMarketSetting
|
|
73
76
|
) {
|
|
74
77
|
this.isSubscribed = false;
|
|
75
78
|
this.program = program;
|
|
@@ -79,6 +82,7 @@ export class PollingDriftClientAccountSubscriber
|
|
|
79
82
|
this.spotMarketIndexes = spotMarketIndexes;
|
|
80
83
|
this.oracleInfos = oracleInfos;
|
|
81
84
|
this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles;
|
|
85
|
+
this.delistedMarketSetting = delistedMarketSetting;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
public async subscribe(): Promise<boolean> {
|
|
@@ -120,6 +124,8 @@ export class PollingDriftClientAccountSubscriber
|
|
|
120
124
|
this.eventEmitter.emit('update');
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
this.handleDelistedMarkets();
|
|
128
|
+
|
|
123
129
|
await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]);
|
|
124
130
|
|
|
125
131
|
this.isSubscribing = false;
|
|
@@ -500,6 +506,36 @@ export class PollingDriftClientAccountSubscriber
|
|
|
500
506
|
await Promise.all(oraclePromises);
|
|
501
507
|
}
|
|
502
508
|
|
|
509
|
+
handleDelistedMarkets(): void {
|
|
510
|
+
if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles(
|
|
515
|
+
this.getMarketAccountsAndSlots(),
|
|
516
|
+
this.getSpotMarketAccountsAndSlots()
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
for (const perpMarketIndex of perpMarketIndexes) {
|
|
520
|
+
const perpMarketPubkey = this.perpMarket.get(perpMarketIndex).data.pubkey;
|
|
521
|
+
const callbackId = this.accountsToPoll.get(
|
|
522
|
+
perpMarketPubkey.toBase58()
|
|
523
|
+
).callbackId;
|
|
524
|
+
this.accountLoader.removeAccount(perpMarketPubkey, callbackId);
|
|
525
|
+
if (this.delistedMarketSetting === DelistedMarketSetting.Discard) {
|
|
526
|
+
this.perpMarket.delete(perpMarketIndex);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for (const oracle of oracles) {
|
|
531
|
+
const callbackId = this.oraclesToPoll.get(oracle.toBase58()).callbackId;
|
|
532
|
+
this.accountLoader.removeAccount(oracle, callbackId);
|
|
533
|
+
if (this.delistedMarketSetting === DelistedMarketSetting.Discard) {
|
|
534
|
+
this.oracles.delete(oracle.toBase58());
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
503
539
|
assertIsSubscribed(): void {
|
|
504
540
|
if (!this.isSubscribed) {
|
|
505
541
|
throw new NotSubscribedError(
|
package/src/accounts/types.ts
CHANGED
|
@@ -84,6 +84,12 @@ export interface DriftClientAccountSubscriber {
|
|
|
84
84
|
updateAccountLoaderPollingFrequency?: (pollingFrequency: number) => void;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export enum DelistedMarketSetting {
|
|
88
|
+
Unsubscribe,
|
|
89
|
+
Subscribe,
|
|
90
|
+
Discard,
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
export interface UserAccountEvents {
|
|
88
94
|
userAccountUpdate: (payload: UserAccount) => void;
|
|
89
95
|
update: void;
|
package/src/accounts/utils.ts
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
import { PublicKey } from '@solana/web3.js';
|
|
2
|
+
import { DataAndSlot } from './types';
|
|
3
|
+
import { isVariant, PerpMarketAccount, SpotMarketAccount } from '../types';
|
|
4
|
+
|
|
1
5
|
export function capitalize(value: string): string {
|
|
2
6
|
return value[0].toUpperCase() + value.slice(1);
|
|
3
7
|
}
|
|
8
|
+
|
|
9
|
+
export function findDelistedPerpMarketsAndOracles(
|
|
10
|
+
perpMarkets: DataAndSlot<PerpMarketAccount>[],
|
|
11
|
+
spotMarkets: DataAndSlot<SpotMarketAccount>[]
|
|
12
|
+
): { perpMarketIndexes: number[]; oracles: PublicKey[] } {
|
|
13
|
+
const delistedPerpMarketIndexes = [];
|
|
14
|
+
const delistedOracles = [];
|
|
15
|
+
for (const perpMarket of perpMarkets) {
|
|
16
|
+
if (!perpMarket.data) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (isVariant(perpMarket.data.status, 'delisted')) {
|
|
21
|
+
delistedPerpMarketIndexes.push(perpMarket.data.marketIndex);
|
|
22
|
+
delistedOracles.push(perpMarket.data.amm.oracle);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// make sure oracle isn't used by spot market
|
|
27
|
+
const filteredDelistedOracles = [];
|
|
28
|
+
for (const delistedOracle of delistedOracles) {
|
|
29
|
+
for (const spotMarket of spotMarkets) {
|
|
30
|
+
if (!spotMarket.data) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (spotMarket.data.oracle.equals(delistedOracle)) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
filteredDelistedOracles.push(delistedOracle);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
perpMarketIndexes: delistedPerpMarketIndexes,
|
|
43
|
+
oracles: filteredDelistedOracles,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
DriftClientAccountEvents,
|
|
2
|
+
AccountSubscriber,
|
|
4
3
|
DataAndSlot,
|
|
4
|
+
DelistedMarketSetting,
|
|
5
|
+
DriftClientAccountEvents,
|
|
6
|
+
DriftClientAccountSubscriber,
|
|
7
|
+
NotSubscribedError,
|
|
5
8
|
ResubOpts,
|
|
6
9
|
} from './types';
|
|
7
|
-
import {
|
|
8
|
-
import { SpotMarketAccount, PerpMarketAccount, StateAccount } from '../types';
|
|
10
|
+
import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types';
|
|
9
11
|
import { Program } from '@coral-xyz/anchor';
|
|
10
12
|
import StrictEventEmitter from 'strict-event-emitter-types';
|
|
11
13
|
import { EventEmitter } from 'events';
|
|
12
14
|
import {
|
|
13
15
|
getDriftStateAccountPublicKey,
|
|
14
|
-
getSpotMarketPublicKey,
|
|
15
16
|
getPerpMarketPublicKey,
|
|
16
17
|
getPerpMarketPublicKeySync,
|
|
18
|
+
getSpotMarketPublicKey,
|
|
17
19
|
getSpotMarketPublicKeySync,
|
|
18
20
|
} from '../addresses/pda';
|
|
19
21
|
import { WebSocketAccountSubscriber } from './webSocketAccountSubscriber';
|
|
@@ -23,6 +25,7 @@ import { OracleClientCache } from '../oracles/oracleClientCache';
|
|
|
23
25
|
import * as Buffer from 'buffer';
|
|
24
26
|
import { QUOTE_ORACLE_PRICE_DATA } from '../oracles/quoteAssetOracleClient';
|
|
25
27
|
import { findAllMarketAndOracles } from '../config';
|
|
28
|
+
import { findDelistedPerpMarketsAndOracles } from './utils';
|
|
26
29
|
|
|
27
30
|
const ORACLE_DEFAULT_KEY = PublicKey.default.toBase58();
|
|
28
31
|
|
|
@@ -55,6 +58,7 @@ export class WebSocketDriftClientAccountSubscriber
|
|
|
55
58
|
spotOracleMap = new Map<number, PublicKey>();
|
|
56
59
|
spotOracleStringMap = new Map<number, string>();
|
|
57
60
|
oracleSubscribers = new Map<string, AccountSubscriber<OraclePriceData>>();
|
|
61
|
+
delistedMarketSetting: DelistedMarketSetting;
|
|
58
62
|
|
|
59
63
|
initialPerpMarketAccountData: Map<number, PerpMarketAccount>;
|
|
60
64
|
initialSpotMarketAccountData: Map<number, SpotMarketAccount>;
|
|
@@ -70,6 +74,7 @@ export class WebSocketDriftClientAccountSubscriber
|
|
|
70
74
|
spotMarketIndexes: number[],
|
|
71
75
|
oracleInfos: OracleInfo[],
|
|
72
76
|
shouldFindAllMarketsAndOracles: boolean,
|
|
77
|
+
delistedMarketSetting: DelistedMarketSetting,
|
|
73
78
|
resubOpts?: ResubOpts,
|
|
74
79
|
commitment?: Commitment
|
|
75
80
|
) {
|
|
@@ -80,6 +85,7 @@ export class WebSocketDriftClientAccountSubscriber
|
|
|
80
85
|
this.spotMarketIndexes = spotMarketIndexes;
|
|
81
86
|
this.oracleInfos = oracleInfos;
|
|
82
87
|
this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles;
|
|
88
|
+
this.delistedMarketSetting = delistedMarketSetting;
|
|
83
89
|
this.resubOpts = resubOpts;
|
|
84
90
|
this.commitment = commitment;
|
|
85
91
|
}
|
|
@@ -151,6 +157,8 @@ export class WebSocketDriftClientAccountSubscriber
|
|
|
151
157
|
|
|
152
158
|
this.eventEmitter.emit('update');
|
|
153
159
|
|
|
160
|
+
await this.handleDelistedMarkets();
|
|
161
|
+
|
|
154
162
|
await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]);
|
|
155
163
|
|
|
156
164
|
this.isSubscribing = false;
|
|
@@ -480,6 +488,33 @@ export class WebSocketDriftClientAccountSubscriber
|
|
|
480
488
|
await Promise.all(addOraclePromises);
|
|
481
489
|
}
|
|
482
490
|
|
|
491
|
+
async handleDelistedMarkets(): Promise<void> {
|
|
492
|
+
if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles(
|
|
497
|
+
this.getMarketAccountsAndSlots(),
|
|
498
|
+
this.getSpotMarketAccountsAndSlots()
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
for (const perpMarketIndex of perpMarketIndexes) {
|
|
502
|
+
await this.perpMarketAccountSubscribers
|
|
503
|
+
.get(perpMarketIndex)
|
|
504
|
+
.unsubscribe();
|
|
505
|
+
if (this.delistedMarketSetting === DelistedMarketSetting.Discard) {
|
|
506
|
+
this.perpMarketAccountSubscribers.delete(perpMarketIndex);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const oracle of oracles) {
|
|
511
|
+
await this.oracleSubscribers.get(oracle.toBase58()).unsubscribe();
|
|
512
|
+
if (this.delistedMarketSetting === DelistedMarketSetting.Discard) {
|
|
513
|
+
this.oracleSubscribers.delete(oracle.toBase58());
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
483
518
|
assertIsSubscribed(): void {
|
|
484
519
|
if (!this.isSubscribed) {
|
|
485
520
|
throw new NotSubscribedError(
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ConfirmOptions } from '@solana/web3.js';
|
|
2
|
+
import { PerpMarketAccount, PublicKey, SpotMarketAccount } from '.';
|
|
2
3
|
import {
|
|
3
4
|
DevnetPerpMarkets,
|
|
4
5
|
MainnetPerpMarkets,
|
|
@@ -13,6 +14,10 @@ import {
|
|
|
13
14
|
} from './constants/spotMarkets';
|
|
14
15
|
import { OracleInfo } from './oracles/types';
|
|
15
16
|
import { Program, ProgramAccount } from '@coral-xyz/anchor';
|
|
17
|
+
import {
|
|
18
|
+
ON_DEMAND_DEVNET_PID,
|
|
19
|
+
ON_DEMAND_MAINNET_PID,
|
|
20
|
+
} from '@switchboard-xyz/on-demand';
|
|
16
21
|
|
|
17
22
|
type DriftConfig = {
|
|
18
23
|
ENV: DriftEnv;
|
|
@@ -30,6 +35,7 @@ type DriftConfig = {
|
|
|
30
35
|
MARKET_LOOKUP_TABLE: string;
|
|
31
36
|
SERUM_LOOKUP_TABLE?: string;
|
|
32
37
|
PYTH_PULL_ORACLE_LOOKUP_TABLE?: string;
|
|
38
|
+
SB_ON_DEMAND_PID: PublicKey;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
export type DriftEnv = 'devnet' | 'mainnet-beta';
|
|
@@ -37,6 +43,14 @@ export type DriftEnv = 'devnet' | 'mainnet-beta';
|
|
|
37
43
|
export const DRIFT_PROGRAM_ID = 'dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH';
|
|
38
44
|
export const DRIFT_ORACLE_RECEIVER_ID =
|
|
39
45
|
'G6EoTTTgpkNBtVXo96EQp2m6uwwVh2Kt6YidjkmQqoha';
|
|
46
|
+
export const SWIFT_ID = 'SW1fThqrxLzVprnCMpiybiqYQfoNCdduC5uWsSUKChS';
|
|
47
|
+
export const ANCHOR_TEST_SWIFT_ID =
|
|
48
|
+
'DpaEdAPW3ZX67fnczT14AoX12Lx9VMkxvtT81nCHy3Nv';
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_CONFIRMATION_OPTS: ConfirmOptions = {
|
|
51
|
+
preflightCommitment: 'confirmed',
|
|
52
|
+
commitment: 'confirmed',
|
|
53
|
+
};
|
|
40
54
|
|
|
41
55
|
export const configs: { [key in DriftEnv]: DriftConfig } = {
|
|
42
56
|
devnet: {
|
|
@@ -54,6 +68,7 @@ export const configs: { [key in DriftEnv]: DriftConfig } = {
|
|
|
54
68
|
SPOT_MARKETS: DevnetSpotMarkets,
|
|
55
69
|
MARKET_LOOKUP_TABLE: 'FaMS3U4uBojvGn5FSDEPimddcXsCfwkKsFgMVVnDdxGb',
|
|
56
70
|
DRIFT_ORACLE_RECEIVER_ID,
|
|
71
|
+
SB_ON_DEMAND_PID: ON_DEMAND_DEVNET_PID,
|
|
57
72
|
},
|
|
58
73
|
'mainnet-beta': {
|
|
59
74
|
ENV: 'mainnet-beta',
|
|
@@ -71,6 +86,7 @@ export const configs: { [key in DriftEnv]: DriftConfig } = {
|
|
|
71
86
|
MARKET_LOOKUP_TABLE: 'D9cnvzswDikQDf53k4HpQ3KJ9y1Fv3HGGDFYMXnK5T6c',
|
|
72
87
|
SERUM_LOOKUP_TABLE: 'GPZkp76cJtNL2mphCvT6FXkJCVPpouidnacckR6rzKDN',
|
|
73
88
|
DRIFT_ORACLE_RECEIVER_ID,
|
|
89
|
+
SB_ON_DEMAND_PID: ON_DEMAND_MAINNET_PID,
|
|
74
90
|
},
|
|
75
91
|
};
|
|
76
92
|
|
|
@@ -297,7 +297,7 @@ export const DevnetPerpMarkets: PerpMarketConfig[] = [
|
|
|
297
297
|
symbol: 'W-PERP',
|
|
298
298
|
baseAssetSymbol: 'W',
|
|
299
299
|
marketIndex: 23,
|
|
300
|
-
oracle: new PublicKey('
|
|
300
|
+
oracle: new PublicKey('J9nrFWjDUeDVZ4BhhxsbQXWgLcLEgQyNBrCbwSADmJdr'),
|
|
301
301
|
launchTs: 1709852537000,
|
|
302
302
|
oracleSource: OracleSource.SWITCHBOARD_ON_DEMAND,
|
|
303
303
|
pythFeedId:
|
|
@@ -862,6 +862,40 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [
|
|
|
862
862
|
launchTs: 1726646453000,
|
|
863
863
|
oracleSource: OracleSource.Prelaunch,
|
|
864
864
|
},
|
|
865
|
+
{
|
|
866
|
+
fullName: 'MOTHER',
|
|
867
|
+
category: ['Solana', 'Meme'],
|
|
868
|
+
symbol: 'MOTHER-PERP',
|
|
869
|
+
baseAssetSymbol: 'MOTHER',
|
|
870
|
+
marketIndex: 44,
|
|
871
|
+
oracle: new PublicKey('56ap2coZG7FPWUigVm9XrpQs3xuCwnwQaWtjWZcffEUG'),
|
|
872
|
+
launchTs: 1727291859000,
|
|
873
|
+
oracleSource: OracleSource.PYTH_PULL,
|
|
874
|
+
pythFeedId:
|
|
875
|
+
'0x62742a997d01f7524f791fdb2dd43aaf0e567d765ebf8fd0406a994239e874d4',
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
fullName: 'MOODENG',
|
|
879
|
+
category: ['Solana', 'Meme'],
|
|
880
|
+
symbol: 'MOODENG-PERP',
|
|
881
|
+
baseAssetSymbol: 'MOODENG',
|
|
882
|
+
marketIndex: 45,
|
|
883
|
+
oracle: new PublicKey('21gjgEcuDppthwV16J1QpFzje3vmgMp2uSzh7pJsG7ob'),
|
|
884
|
+
launchTs: 1727965864000,
|
|
885
|
+
oracleSource: OracleSource.PYTH_PULL,
|
|
886
|
+
pythFeedId:
|
|
887
|
+
'0xffff73128917a90950cd0473fd2551d7cd274fd5a6cc45641881bbcc6ee73417',
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
fullName: 'WARWICK-FIGHT-WIN-BET',
|
|
891
|
+
category: ['Prediction', 'Sport'],
|
|
892
|
+
symbol: 'WARWICK-FIGHT-WIN-BET',
|
|
893
|
+
baseAssetSymbol: 'WARWICK-FIGHT-WIN',
|
|
894
|
+
marketIndex: 46,
|
|
895
|
+
oracle: new PublicKey('Dz5Nvxo1hv7Zfyu11hy8e97twLMRKk6heTWCDGXytj7N'),
|
|
896
|
+
launchTs: 1727965864000,
|
|
897
|
+
oracleSource: OracleSource.Prelaunch,
|
|
898
|
+
},
|
|
865
899
|
];
|
|
866
900
|
|
|
867
901
|
export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = {
|
|
@@ -425,6 +425,17 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [
|
|
|
425
425
|
pythFeedId:
|
|
426
426
|
'0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d',
|
|
427
427
|
},
|
|
428
|
+
{
|
|
429
|
+
symbol: 'MOTHER',
|
|
430
|
+
marketIndex: 26,
|
|
431
|
+
oracle: new PublicKey('56ap2coZG7FPWUigVm9XrpQs3xuCwnwQaWtjWZcffEUG'),
|
|
432
|
+
oracleSource: OracleSource.PYTH_PULL,
|
|
433
|
+
mint: new PublicKey('3S8qX1MsMqRbiwKg2cQyx7nis1oHMgaCuc9c4VfvVdPN'),
|
|
434
|
+
precision: new BN(10).pow(SIX),
|
|
435
|
+
precisionExp: SIX,
|
|
436
|
+
pythFeedId:
|
|
437
|
+
'0x62742a997d01f7524f791fdb2dd43aaf0e567d765ebf8fd0406a994239e874d4',
|
|
438
|
+
},
|
|
428
439
|
];
|
|
429
440
|
|
|
430
441
|
export const SpotMarkets: { [key in DriftEnv]: SpotMarketConfig[] } = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NOT_CONFIRMED_ERROR_CODE = -1001;
|