@drift-labs/sdk-browser 2.155.0-beta.3 → 2.155.0-beta.5
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/VERSION +1 -1
- package/lib/browser/decode/user.js +8 -5
- package/lib/browser/driftClient.d.ts +15 -10
- package/lib/browser/driftClient.js +137 -23
- package/lib/browser/marginCalculation.d.ts +0 -12
- package/lib/browser/marginCalculation.js +0 -20
- package/lib/browser/math/margin.js +1 -0
- package/lib/browser/math/position.d.ts +1 -0
- package/lib/browser/math/position.js +10 -2
- package/lib/browser/swap/UnifiedSwapClient.js +1 -10
- package/lib/browser/titan/titanClient.d.ts +4 -5
- package/lib/browser/titan/titanClient.js +2 -16
- package/lib/browser/types.d.ts +9 -6
- package/lib/browser/types.js +11 -7
- package/lib/browser/user.js +13 -7
- package/lib/node/decode/user.d.ts.map +1 -1
- package/lib/node/decode/user.js +8 -5
- package/lib/node/driftClient.d.ts +15 -10
- package/lib/node/driftClient.d.ts.map +1 -1
- package/lib/node/driftClient.js +137 -23
- package/lib/node/marginCalculation.d.ts +0 -12
- package/lib/node/marginCalculation.d.ts.map +1 -1
- package/lib/node/marginCalculation.js +0 -20
- package/lib/node/math/margin.d.ts.map +1 -1
- package/lib/node/math/margin.js +1 -0
- package/lib/node/math/position.d.ts +1 -0
- package/lib/node/math/position.d.ts.map +1 -1
- package/lib/node/math/position.js +10 -2
- package/lib/node/math/spotBalance.d.ts.map +1 -1
- package/lib/node/swap/UnifiedSwapClient.d.ts.map +1 -1
- package/lib/node/swap/UnifiedSwapClient.js +1 -10
- package/lib/node/titan/titanClient.d.ts +4 -5
- package/lib/node/titan/titanClient.d.ts.map +1 -1
- package/lib/node/titan/titanClient.js +2 -16
- package/lib/node/types.d.ts +9 -6
- package/lib/node/types.d.ts.map +1 -1
- package/lib/node/types.js +11 -7
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +13 -7
- package/package.json +1 -1
- package/scripts/deposit-isolated-positions.ts +110 -0
- package/scripts/find-flagged-users.ts +216 -0
- package/scripts/single-grpc-client-test.ts +71 -21
- package/scripts/withdraw-isolated-positions.ts +174 -0
- package/src/decode/user.ts +14 -6
- package/src/driftClient.ts +297 -65
- package/src/margin/README.md +139 -0
- package/src/marginCalculation.ts +0 -32
- package/src/math/margin.ts +2 -3
- package/src/math/position.ts +12 -2
- package/src/math/spotBalance.ts +0 -1
- package/src/swap/UnifiedSwapClient.ts +2 -13
- package/src/titan/titanClient.ts +4 -28
- package/src/types.ts +11 -7
- package/src/user.ts +17 -8
- package/tests/dlob/helpers.ts +1 -1
- package/tests/user/test.ts +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import {
|
|
4
|
+
DriftClient,
|
|
5
|
+
DriftClientConfig,
|
|
6
|
+
Wallet,
|
|
7
|
+
UserMap,
|
|
8
|
+
DRIFT_PROGRAM_ID,
|
|
9
|
+
getMarketsAndOraclesForSubscription,
|
|
10
|
+
BulkAccountLoader,
|
|
11
|
+
BN,
|
|
12
|
+
PerpPosition,
|
|
13
|
+
} from '../src';
|
|
14
|
+
import { TransactionSignature } from '@solana/web3.js';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
dotenv.config({ path: '../' });
|
|
21
|
+
// Simple CLI parsing
|
|
22
|
+
interface CliOptions {
|
|
23
|
+
mode: 'list' | 'one' | 'all';
|
|
24
|
+
targetUser?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseCliArgs(): CliOptions {
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
let mode: CliOptions['mode'] = 'list';
|
|
30
|
+
let targetUser: string | undefined = undefined;
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const arg = args[i];
|
|
33
|
+
if (arg === '--mode' && i + 1 < args.length) {
|
|
34
|
+
const next = args[i + 1] as CliOptions['mode'];
|
|
35
|
+
if (next === 'list' || next === 'one' || next === 'all') {
|
|
36
|
+
mode = next;
|
|
37
|
+
}
|
|
38
|
+
i++;
|
|
39
|
+
} else if ((arg === '--user' || arg === '--target') && i + 1 < args.length) {
|
|
40
|
+
targetUser = args[i + 1];
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { mode, targetUser };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { mode, targetUser } = parseCliArgs();
|
|
48
|
+
|
|
49
|
+
const RPC_ENDPOINT =
|
|
50
|
+
process.env.RPC_ENDPOINT ?? 'https://api.mainnet-beta.solana.com';
|
|
51
|
+
|
|
52
|
+
const connection = new Connection(RPC_ENDPOINT);
|
|
53
|
+
const keypairPath =
|
|
54
|
+
process.env.SOLANA_KEYPAIR ??
|
|
55
|
+
path.join(os.homedir(), '.config', 'solana', 'id.json');
|
|
56
|
+
const secret = JSON.parse(fs.readFileSync(keypairPath, 'utf-8')) as number[];
|
|
57
|
+
const wallet = new Wallet(Keypair.fromSecretKey(Uint8Array.from(secret)));
|
|
58
|
+
|
|
59
|
+
const { perpMarketIndexes, spotMarketIndexes, oracleInfos } =
|
|
60
|
+
getMarketsAndOraclesForSubscription('mainnet-beta');
|
|
61
|
+
|
|
62
|
+
const accountLoader = new BulkAccountLoader(connection, 'confirmed', 60_000);
|
|
63
|
+
|
|
64
|
+
const clientConfig: DriftClientConfig = {
|
|
65
|
+
connection,
|
|
66
|
+
wallet,
|
|
67
|
+
programID: new PublicKey(DRIFT_PROGRAM_ID),
|
|
68
|
+
accountSubscription: {
|
|
69
|
+
type: 'polling',
|
|
70
|
+
accountLoader,
|
|
71
|
+
},
|
|
72
|
+
perpMarketIndexes,
|
|
73
|
+
spotMarketIndexes,
|
|
74
|
+
oracleInfos,
|
|
75
|
+
env: 'mainnet-beta',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const client = new DriftClient(clientConfig);
|
|
79
|
+
await client.subscribe();
|
|
80
|
+
|
|
81
|
+
const userMap = new UserMap({
|
|
82
|
+
driftClient: client,
|
|
83
|
+
subscriptionConfig: {
|
|
84
|
+
type: 'polling',
|
|
85
|
+
frequency: 60_000,
|
|
86
|
+
commitment: 'confirmed',
|
|
87
|
+
},
|
|
88
|
+
includeIdle: false,
|
|
89
|
+
syncConfig: { type: 'paginated' },
|
|
90
|
+
throwOnFailedSync: false,
|
|
91
|
+
});
|
|
92
|
+
await userMap.subscribe();
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
const flaggedUsers: Array<{
|
|
96
|
+
userPubkey: string;
|
|
97
|
+
authority: string;
|
|
98
|
+
flags: Array<{ marketIndex: number; flag: number; isolatedPositionScaledBalance: BN }>;
|
|
99
|
+
}> = [];
|
|
100
|
+
|
|
101
|
+
console.log(`User map size: ${Array.from(userMap.entries()).length}`);
|
|
102
|
+
|
|
103
|
+
for (const [userPubkey, user] of userMap.entries()) {
|
|
104
|
+
const userAccount = user.getUserAccount();
|
|
105
|
+
const flaggedPositions = userAccount.perpPositions
|
|
106
|
+
.filter((p) => p.positionFlag >= 1 || p.isolatedPositionScaledBalance.toString() !== '0')
|
|
107
|
+
.map((p) => ({ marketIndex: p.marketIndex, flag: p.positionFlag, isolatedPositionScaledBalance: p.isolatedPositionScaledBalance, fullPosition: p }));
|
|
108
|
+
|
|
109
|
+
if (flaggedPositions.length > 0) {
|
|
110
|
+
if(mode === 'one' && userPubkey === targetUser) {
|
|
111
|
+
console.log(`flagged positions on user ${userPubkey}`);
|
|
112
|
+
console.log(flaggedPositions.map((p) => `mkt=${p.marketIndex}, flag=${p.flag}, isolatedPositionScaledBalance=${p.isolatedPositionScaledBalance.toString()}, fullPosition=${fullLogPerpPosition(p.fullPosition)}`).join('\n\n; '));
|
|
113
|
+
}
|
|
114
|
+
flaggedUsers.push({
|
|
115
|
+
userPubkey,
|
|
116
|
+
authority: userAccount.authority.toBase58(),
|
|
117
|
+
flags: flaggedPositions,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Mode 1: list flagged users (default)
|
|
123
|
+
if (mode === 'list') {
|
|
124
|
+
console.log(`Flagged users (positionFlag >= 1 || isolatedPositionScaledBalance > 0): ${flaggedUsers.length}`);
|
|
125
|
+
for (const u of flaggedUsers) {
|
|
126
|
+
const flagsStr = u.flags
|
|
127
|
+
.map((f) => `mkt=${f.marketIndex}, flag=${f.flag}, isolatedPositionScaledBalance=${f.isolatedPositionScaledBalance.toString()}`)
|
|
128
|
+
.join('; ');
|
|
129
|
+
console.log(
|
|
130
|
+
`- authority=${u.authority} userAccount=${u.userPubkey} -> [${flagsStr}]`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Helper to invoke updateUserIdle
|
|
136
|
+
async function updateUserIdleFor(userAccountPubkeyStr: string): Promise<TransactionSignature | undefined> {
|
|
137
|
+
const userObj = userMap.get(userAccountPubkeyStr);
|
|
138
|
+
if (!userObj) {
|
|
139
|
+
console.warn(`User ${userAccountPubkeyStr} not found in userMap`);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const sig = await client.updateUserIdle(
|
|
144
|
+
new PublicKey(userAccountPubkeyStr),
|
|
145
|
+
userObj.getUserAccount()
|
|
146
|
+
);
|
|
147
|
+
console.log(`updateUserIdle sent for userAccount=${userAccountPubkeyStr} -> tx=${sig}`);
|
|
148
|
+
return sig;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error(`Failed updateUserIdle for userAccount=${userAccountPubkeyStr}`, e);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Mode 2: updateUserIdle on a single flagged user
|
|
156
|
+
if (mode === 'one') {
|
|
157
|
+
if (flaggedUsers.length === 0) {
|
|
158
|
+
console.log('No flagged users to update.');
|
|
159
|
+
} else {
|
|
160
|
+
const chosen =
|
|
161
|
+
(targetUser && flaggedUsers.find((u) => u.userPubkey === targetUser)) ||
|
|
162
|
+
flaggedUsers[0];
|
|
163
|
+
console.log(
|
|
164
|
+
`Updating single flagged userAccount=${chosen.userPubkey} authority=${chosen.authority}`
|
|
165
|
+
);
|
|
166
|
+
await updateUserIdleFor(chosen.userPubkey);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Mode 3: updateUserIdle on all flagged users
|
|
171
|
+
if (mode === 'all') {
|
|
172
|
+
if (flaggedUsers.length === 0) {
|
|
173
|
+
console.log('No flagged users to update.');
|
|
174
|
+
} else {
|
|
175
|
+
console.log(`Updating all ${flaggedUsers.length} flagged users...`);
|
|
176
|
+
for (const u of flaggedUsers) {
|
|
177
|
+
await updateUserIdleFor(u.userPubkey);
|
|
178
|
+
}
|
|
179
|
+
console.log('Finished updating all flagged users.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await userMap.unsubscribe();
|
|
184
|
+
await client.unsubscribe();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch((e) => {
|
|
188
|
+
console.error(e);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
function fullLogPerpPosition(position: PerpPosition) {
|
|
194
|
+
|
|
195
|
+
return `
|
|
196
|
+
[PERP POSITION]
|
|
197
|
+
baseAssetAmount=${position.baseAssetAmount.toString()}
|
|
198
|
+
quoteAssetAmount=${position.quoteAssetAmount.toString()}
|
|
199
|
+
quoteBreakEvenAmount=${position.quoteBreakEvenAmount.toString()}
|
|
200
|
+
quoteEntryAmount=${position.quoteEntryAmount.toString()}
|
|
201
|
+
openBids=${position.openBids.toString()}
|
|
202
|
+
openAsks=${position.openAsks.toString()}
|
|
203
|
+
settledPnl=${position.settledPnl.toString()}
|
|
204
|
+
lpShares=${position.lpShares.toString()}
|
|
205
|
+
remainderBaseAssetAmount=${position.remainderBaseAssetAmount}
|
|
206
|
+
lastQuoteAssetAmountPerLp=${position.lastQuoteAssetAmountPerLp.toString()}
|
|
207
|
+
perLpBase=${position.perLpBase}
|
|
208
|
+
maxMarginRatio=${position.maxMarginRatio}
|
|
209
|
+
marketIndex=${position.marketIndex}
|
|
210
|
+
openOrders=${position.openOrders}
|
|
211
|
+
positionFlag=${position.positionFlag}
|
|
212
|
+
isolatedPositionScaledBalance=${position.isolatedPositionScaledBalance.toString()}
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
@@ -48,9 +48,7 @@ async function initializeSingleGrpcClient() {
|
|
|
48
48
|
const allPerpMarketProgramAccounts =
|
|
49
49
|
(await program.account.perpMarket.all()) as ProgramAccount<PerpMarketAccount>[];
|
|
50
50
|
const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) =>
|
|
51
|
-
[
|
|
52
|
-
val.account.marketIndex
|
|
53
|
-
)
|
|
51
|
+
[46].includes(val.account.marketIndex)
|
|
54
52
|
);
|
|
55
53
|
const perpMarketIndexes = perpMarketProgramAccounts.map(
|
|
56
54
|
(val) => val.account.marketIndex
|
|
@@ -60,7 +58,7 @@ async function initializeSingleGrpcClient() {
|
|
|
60
58
|
const allSpotMarketProgramAccounts =
|
|
61
59
|
(await program.account.spotMarket.all()) as ProgramAccount<SpotMarketAccount>[];
|
|
62
60
|
const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) =>
|
|
63
|
-
[0
|
|
61
|
+
[0].includes(val.account.marketIndex)
|
|
64
62
|
);
|
|
65
63
|
const spotMarketIndexes = spotMarketProgramAccounts.map(
|
|
66
64
|
(val) => val.account.marketIndex
|
|
@@ -94,7 +92,9 @@ async function initializeSingleGrpcClient() {
|
|
|
94
92
|
}
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
console.log(
|
|
95
|
+
console.log(
|
|
96
|
+
`📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot`
|
|
97
|
+
);
|
|
98
98
|
console.log(`🔮 Oracles: ${oracleInfos.length}`);
|
|
99
99
|
|
|
100
100
|
|
|
@@ -171,7 +171,9 @@ async function initializeSingleGrpcClient() {
|
|
|
171
171
|
await client.subscribe();
|
|
172
172
|
|
|
173
173
|
console.log('✅ Client subscribed successfully!');
|
|
174
|
-
console.log(
|
|
174
|
+
console.log(
|
|
175
|
+
'🚀 Starting high-load testing (50 reads/sec per perp market)...'
|
|
176
|
+
);
|
|
175
177
|
|
|
176
178
|
// High-frequency load testing - 50 reads per second per perp market
|
|
177
179
|
const loadTestInterval = setInterval(async () => {
|
|
@@ -179,29 +181,71 @@ async function initializeSingleGrpcClient() {
|
|
|
179
181
|
// Test getPerpMarketAccount for each perp market (50 times per second per market)
|
|
180
182
|
for (const marketIndex of perpMarketIndexes) {
|
|
181
183
|
const perpMarketAccount = client.getPerpMarketAccount(marketIndex);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
if (!perpMarketAccount) {
|
|
185
|
+
console.log(`Perp market ${marketIndex} not found`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
console.log(
|
|
189
|
+
'perpMarketAccount name: ',
|
|
190
|
+
decodeName(perpMarketAccount.name)
|
|
191
|
+
);
|
|
192
|
+
console.log(
|
|
193
|
+
'perpMarketAccount data: ',
|
|
194
|
+
JSON.stringify({
|
|
195
|
+
marketIndex: perpMarketAccount.marketIndex,
|
|
196
|
+
name: decodeName(perpMarketAccount.name),
|
|
197
|
+
baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(),
|
|
198
|
+
quoteAssetReserve:
|
|
199
|
+
perpMarketAccount.amm.quoteAssetReserve.toString(),
|
|
200
|
+
})
|
|
201
|
+
);
|
|
189
202
|
}
|
|
190
203
|
|
|
191
204
|
// Test getMMOracleDataForPerpMarket for each perp market (50 times per second per market)
|
|
192
205
|
for (const marketIndex of perpMarketIndexes) {
|
|
193
206
|
try {
|
|
194
207
|
const oracleData = client.getMMOracleDataForPerpMarket(marketIndex);
|
|
195
|
-
console.log(
|
|
196
|
-
console.log(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
console.log('oracleData price: ', oracleData.price.toString());
|
|
209
|
+
console.log(
|
|
210
|
+
'oracleData: ',
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
price: oracleData.price.toString(),
|
|
213
|
+
confidence: oracleData.confidence?.toString(),
|
|
214
|
+
slot: oracleData.slot?.toString(),
|
|
215
|
+
})
|
|
216
|
+
);
|
|
201
217
|
} catch (error) {
|
|
202
218
|
// Ignore errors for load testing
|
|
203
219
|
}
|
|
204
220
|
}
|
|
221
|
+
|
|
222
|
+
for (const marketIndex of perpMarketIndexes) {
|
|
223
|
+
try {
|
|
224
|
+
const { data, slot } =
|
|
225
|
+
client.accountSubscriber.getMarketAccountAndSlot(marketIndex);
|
|
226
|
+
if (!data) {
|
|
227
|
+
console.log(
|
|
228
|
+
`Perp market getMarketAccountAndSlot ${marketIndex} not found`
|
|
229
|
+
);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
console.log(
|
|
233
|
+
'marketAccountAndSlot: ',
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
marketIndex: data.marketIndex,
|
|
236
|
+
name: decodeName(data.name),
|
|
237
|
+
slot: slot?.toString(),
|
|
238
|
+
baseAssetReserve: data.amm.baseAssetReserve.toString(),
|
|
239
|
+
quoteAssetReserve: data.amm.quoteAssetReserve.toString(),
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(
|
|
244
|
+
`Error getting market account and slot for market ${marketIndex}:`,
|
|
245
|
+
error
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
205
249
|
} catch (error) {
|
|
206
250
|
console.error('Load test error:', error);
|
|
207
251
|
}
|
|
@@ -211,8 +255,14 @@ async function initializeSingleGrpcClient() {
|
|
|
211
255
|
const statsInterval = setInterval(() => {
|
|
212
256
|
console.log('\n📈 Event Counts:', eventCounts);
|
|
213
257
|
console.log(`⏱️ Client subscribed: ${client.isSubscribed}`);
|
|
214
|
-
console.log(
|
|
215
|
-
|
|
258
|
+
console.log(
|
|
259
|
+
`🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}`
|
|
260
|
+
);
|
|
261
|
+
console.log(
|
|
262
|
+
`🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${
|
|
263
|
+
perpMarketIndexes.length
|
|
264
|
+
} markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)`
|
|
265
|
+
);
|
|
216
266
|
}, 5000);
|
|
217
267
|
|
|
218
268
|
// Handle shutdown signals - just exit without cleanup since they never unsubscribe
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import {
|
|
4
|
+
AnchorProvider,
|
|
5
|
+
Idl,
|
|
6
|
+
Program,
|
|
7
|
+
ProgramAccount,
|
|
8
|
+
} from '@coral-xyz/anchor';
|
|
9
|
+
import driftIDL from '../src/idl/drift.json';
|
|
10
|
+
import {
|
|
11
|
+
DRIFT_PROGRAM_ID,
|
|
12
|
+
PerpMarketAccount,
|
|
13
|
+
SpotMarketAccount,
|
|
14
|
+
OracleInfo,
|
|
15
|
+
Wallet,
|
|
16
|
+
ZERO,
|
|
17
|
+
} from '../src';
|
|
18
|
+
import { DriftClient } from '../src/driftClient';
|
|
19
|
+
import { DriftClientConfig } from '../src/driftClientConfig';
|
|
20
|
+
|
|
21
|
+
function isStatusOpen(status: any) {
|
|
22
|
+
return !!status && 'open' in status;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPerpMarketType(marketType: any) {
|
|
26
|
+
return !!marketType && 'perp' in marketType;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
dotenv.config({ path: '../' });
|
|
31
|
+
|
|
32
|
+
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
|
|
33
|
+
if (!RPC_ENDPOINT) throw new Error('RPC_ENDPOINT env var required');
|
|
34
|
+
|
|
35
|
+
// Load wallet
|
|
36
|
+
// For safety this creates a new ephemeral wallet unless PRIVATE_KEY is provided (base58 array)
|
|
37
|
+
let keypair: Keypair;
|
|
38
|
+
const pk = process.env.PRIVATE_KEY;
|
|
39
|
+
if (pk) {
|
|
40
|
+
const secret = Uint8Array.from(JSON.parse(pk));
|
|
41
|
+
keypair = Keypair.fromSecretKey(secret);
|
|
42
|
+
} else {
|
|
43
|
+
keypair = new Keypair();
|
|
44
|
+
console.warn(
|
|
45
|
+
'Using ephemeral keypair. Provide PRIVATE_KEY for real withdrawals.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const wallet = new Wallet(keypair);
|
|
49
|
+
|
|
50
|
+
// Connection and program for market discovery
|
|
51
|
+
const connection = new Connection(RPC_ENDPOINT);
|
|
52
|
+
const provider = new AnchorProvider(connection, wallet as any, {
|
|
53
|
+
commitment: 'processed',
|
|
54
|
+
});
|
|
55
|
+
const programId = new PublicKey(DRIFT_PROGRAM_ID);
|
|
56
|
+
const program = new Program(driftIDL as Idl, programId, provider);
|
|
57
|
+
|
|
58
|
+
// Discover markets and oracles (like the example test script)
|
|
59
|
+
const allPerpMarketProgramAccounts =
|
|
60
|
+
(await program.account.perpMarket.all()) as ProgramAccount<PerpMarketAccount>[];
|
|
61
|
+
const perpMarketIndexes = allPerpMarketProgramAccounts.map(
|
|
62
|
+
(val) => val.account.marketIndex
|
|
63
|
+
);
|
|
64
|
+
const allSpotMarketProgramAccounts =
|
|
65
|
+
(await program.account.spotMarket.all()) as ProgramAccount<SpotMarketAccount>[];
|
|
66
|
+
const spotMarketIndexes = allSpotMarketProgramAccounts.map(
|
|
67
|
+
(val) => val.account.marketIndex
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
const oracleInfos: OracleInfo[] = [];
|
|
72
|
+
for (const acct of allPerpMarketProgramAccounts) {
|
|
73
|
+
const key = `${acct.account.amm.oracle.toBase58()}-${
|
|
74
|
+
Object.keys(acct.account.amm.oracleSource)[0]
|
|
75
|
+
}`;
|
|
76
|
+
if (!seen.has(key)) {
|
|
77
|
+
seen.add(key);
|
|
78
|
+
oracleInfos.push({
|
|
79
|
+
publicKey: acct.account.amm.oracle,
|
|
80
|
+
source: acct.account.amm.oracleSource,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const acct of allSpotMarketProgramAccounts) {
|
|
85
|
+
const key = `${acct.account.oracle.toBase58()}-${
|
|
86
|
+
Object.keys(acct.account.oracleSource)[0]
|
|
87
|
+
}`;
|
|
88
|
+
if (!seen.has(key)) {
|
|
89
|
+
seen.add(key);
|
|
90
|
+
oracleInfos.push({
|
|
91
|
+
publicKey: acct.account.oracle,
|
|
92
|
+
source: acct.account.oracleSource,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build DriftClient with websocket subscription (lightweight)
|
|
98
|
+
const clientConfig: DriftClientConfig = {
|
|
99
|
+
connection,
|
|
100
|
+
wallet,
|
|
101
|
+
programID: programId,
|
|
102
|
+
accountSubscription: {
|
|
103
|
+
type: 'websocket',
|
|
104
|
+
commitment: 'processed',
|
|
105
|
+
},
|
|
106
|
+
perpMarketIndexes,
|
|
107
|
+
spotMarketIndexes,
|
|
108
|
+
oracleInfos,
|
|
109
|
+
env: 'devnet',
|
|
110
|
+
};
|
|
111
|
+
const client = new DriftClient(clientConfig);
|
|
112
|
+
await client.subscribe();
|
|
113
|
+
|
|
114
|
+
// Ensure user exists and is subscribed
|
|
115
|
+
const user = client.getUser();
|
|
116
|
+
await user.subscribe();
|
|
117
|
+
|
|
118
|
+
const userAccount = user.getUserAccount();
|
|
119
|
+
const openOrders = user.getOpenOrders();
|
|
120
|
+
|
|
121
|
+
const marketsWithOpenOrders = new Set<number>();
|
|
122
|
+
for (const o of openOrders ?? []) {
|
|
123
|
+
if (isStatusOpen(o.status) && isPerpMarketType(o.marketType)) {
|
|
124
|
+
marketsWithOpenOrders.add(o.marketIndex);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const withdrawTargets = userAccount.perpPositions.filter((pos) => {
|
|
129
|
+
const isZeroBase = pos.baseAssetAmount.eq(ZERO);
|
|
130
|
+
const hasIso = pos.isolatedPositionScaledBalance.gt(ZERO);
|
|
131
|
+
const hasOpenOrders = marketsWithOpenOrders.has(pos.marketIndex);
|
|
132
|
+
return isZeroBase && hasIso && !hasOpenOrders;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log(
|
|
136
|
+
`Found ${withdrawTargets.length} isolated perp positions to withdraw`
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
for (const pos of withdrawTargets) {
|
|
140
|
+
try {
|
|
141
|
+
const amount = client.getIsolatedPerpPositionTokenAmount(pos.marketIndex);
|
|
142
|
+
if (amount.lte(ZERO)) continue;
|
|
143
|
+
|
|
144
|
+
const perpMarketAccount = client.getPerpMarketAccount(pos.marketIndex);
|
|
145
|
+
const quoteAta = await client.getAssociatedTokenAccount(
|
|
146
|
+
perpMarketAccount.quoteSpotMarketIndex
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const ixs = await client.getWithdrawFromIsolatedPerpPositionIxsBundle(
|
|
150
|
+
amount,
|
|
151
|
+
pos.marketIndex,
|
|
152
|
+
0,
|
|
153
|
+
quoteAta,
|
|
154
|
+
true
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const tx = await client.buildTransaction(ixs);
|
|
158
|
+
const { txSig } = await client.sendTransaction(tx);
|
|
159
|
+
console.log(
|
|
160
|
+
`Withdrew isolated deposit for perp market ${pos.marketIndex}: ${txSig}`
|
|
161
|
+
);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error(`Failed to withdraw for market ${pos.marketIndex}:`, e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await user.unsubscribe();
|
|
168
|
+
await client.unsubscribe();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch((e) => {
|
|
172
|
+
console.error(e);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
package/src/decode/user.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
OrderType,
|
|
9
9
|
PerpPosition,
|
|
10
10
|
PositionDirection,
|
|
11
|
+
PositionFlag,
|
|
11
12
|
SpotBalanceType,
|
|
12
13
|
SpotPosition,
|
|
13
14
|
UserAccount,
|
|
@@ -83,6 +84,10 @@ export function decodeUser(buffer: Buffer): UserAccount {
|
|
|
83
84
|
const baseAssetAmount = readSignedBigInt64LE(buffer, offset + 8);
|
|
84
85
|
const quoteAssetAmount = readSignedBigInt64LE(buffer, offset + 16);
|
|
85
86
|
const lpShares = readUnsignedBigInt64LE(buffer, offset + 64);
|
|
87
|
+
const isolatedPositionScaledBalance = readSignedBigInt64LE(
|
|
88
|
+
buffer,
|
|
89
|
+
offset + 72
|
|
90
|
+
);
|
|
86
91
|
const openOrders = buffer.readUInt8(offset + 94);
|
|
87
92
|
const positionFlag = buffer.readUInt8(offset + 95);
|
|
88
93
|
|
|
@@ -90,7 +95,13 @@ export function decodeUser(buffer: Buffer): UserAccount {
|
|
|
90
95
|
baseAssetAmount.eq(ZERO) &&
|
|
91
96
|
openOrders === 0 &&
|
|
92
97
|
quoteAssetAmount.eq(ZERO) &&
|
|
93
|
-
lpShares.eq(ZERO)
|
|
98
|
+
lpShares.eq(ZERO) &&
|
|
99
|
+
isolatedPositionScaledBalance.eq(ZERO) &&
|
|
100
|
+
!(
|
|
101
|
+
(positionFlag &
|
|
102
|
+
(PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) >
|
|
103
|
+
0
|
|
104
|
+
)
|
|
94
105
|
) {
|
|
95
106
|
offset += 96;
|
|
96
107
|
continue;
|
|
@@ -107,9 +118,7 @@ export function decodeUser(buffer: Buffer): UserAccount {
|
|
|
107
118
|
const openAsks = readSignedBigInt64LE(buffer, offset);
|
|
108
119
|
offset += 8;
|
|
109
120
|
const settledPnl = readSignedBigInt64LE(buffer, offset);
|
|
110
|
-
offset +=
|
|
111
|
-
const isolatedPositionScaledBalance = readSignedBigInt64LE(buffer, offset);
|
|
112
|
-
offset += 8;
|
|
121
|
+
offset += 24;
|
|
113
122
|
const lastQuoteAssetAmountPerLp = readSignedBigInt64LE(buffer, offset);
|
|
114
123
|
offset += 8;
|
|
115
124
|
const maxMarginRatio = buffer.readUInt16LE(offset);
|
|
@@ -118,7 +127,6 @@ export function decodeUser(buffer: Buffer): UserAccount {
|
|
|
118
127
|
offset += 3;
|
|
119
128
|
const perLpBase = buffer.readUInt8(offset);
|
|
120
129
|
offset += 1;
|
|
121
|
-
|
|
122
130
|
perpPositions.push({
|
|
123
131
|
lastCumulativeFundingRate,
|
|
124
132
|
baseAssetAmount,
|
|
@@ -135,8 +143,8 @@ export function decodeUser(buffer: Buffer): UserAccount {
|
|
|
135
143
|
openOrders,
|
|
136
144
|
perLpBase,
|
|
137
145
|
maxMarginRatio,
|
|
138
|
-
isolatedPositionScaledBalance,
|
|
139
146
|
positionFlag,
|
|
147
|
+
isolatedPositionScaledBalance,
|
|
140
148
|
});
|
|
141
149
|
}
|
|
142
150
|
|