@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.
Files changed (57) hide show
  1. package/VERSION +1 -1
  2. package/lib/browser/decode/user.js +8 -5
  3. package/lib/browser/driftClient.d.ts +15 -10
  4. package/lib/browser/driftClient.js +137 -23
  5. package/lib/browser/marginCalculation.d.ts +0 -12
  6. package/lib/browser/marginCalculation.js +0 -20
  7. package/lib/browser/math/margin.js +1 -0
  8. package/lib/browser/math/position.d.ts +1 -0
  9. package/lib/browser/math/position.js +10 -2
  10. package/lib/browser/swap/UnifiedSwapClient.js +1 -10
  11. package/lib/browser/titan/titanClient.d.ts +4 -5
  12. package/lib/browser/titan/titanClient.js +2 -16
  13. package/lib/browser/types.d.ts +9 -6
  14. package/lib/browser/types.js +11 -7
  15. package/lib/browser/user.js +13 -7
  16. package/lib/node/decode/user.d.ts.map +1 -1
  17. package/lib/node/decode/user.js +8 -5
  18. package/lib/node/driftClient.d.ts +15 -10
  19. package/lib/node/driftClient.d.ts.map +1 -1
  20. package/lib/node/driftClient.js +137 -23
  21. package/lib/node/marginCalculation.d.ts +0 -12
  22. package/lib/node/marginCalculation.d.ts.map +1 -1
  23. package/lib/node/marginCalculation.js +0 -20
  24. package/lib/node/math/margin.d.ts.map +1 -1
  25. package/lib/node/math/margin.js +1 -0
  26. package/lib/node/math/position.d.ts +1 -0
  27. package/lib/node/math/position.d.ts.map +1 -1
  28. package/lib/node/math/position.js +10 -2
  29. package/lib/node/math/spotBalance.d.ts.map +1 -1
  30. package/lib/node/swap/UnifiedSwapClient.d.ts.map +1 -1
  31. package/lib/node/swap/UnifiedSwapClient.js +1 -10
  32. package/lib/node/titan/titanClient.d.ts +4 -5
  33. package/lib/node/titan/titanClient.d.ts.map +1 -1
  34. package/lib/node/titan/titanClient.js +2 -16
  35. package/lib/node/types.d.ts +9 -6
  36. package/lib/node/types.d.ts.map +1 -1
  37. package/lib/node/types.js +11 -7
  38. package/lib/node/user.d.ts.map +1 -1
  39. package/lib/node/user.js +13 -7
  40. package/package.json +1 -1
  41. package/scripts/deposit-isolated-positions.ts +110 -0
  42. package/scripts/find-flagged-users.ts +216 -0
  43. package/scripts/single-grpc-client-test.ts +71 -21
  44. package/scripts/withdraw-isolated-positions.ts +174 -0
  45. package/src/decode/user.ts +14 -6
  46. package/src/driftClient.ts +297 -65
  47. package/src/margin/README.md +139 -0
  48. package/src/marginCalculation.ts +0 -32
  49. package/src/math/margin.ts +2 -3
  50. package/src/math/position.ts +12 -2
  51. package/src/math/spotBalance.ts +0 -1
  52. package/src/swap/UnifiedSwapClient.ts +2 -13
  53. package/src/titan/titanClient.ts +4 -28
  54. package/src/types.ts +11 -7
  55. package/src/user.ts +17 -8
  56. package/tests/dlob/helpers.ts +1 -1
  57. 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
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes(
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, 1, 2, 3, 4, 5].includes(val.account.marketIndex)
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(`📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot`);
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('🚀 Starting high-load testing (50 reads/sec per perp market)...');
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
- console.log("perpMarketAccount name: ", decodeName(perpMarketAccount.name));
183
- console.log("perpMarketAccount data: ", JSON.stringify({
184
- marketIndex: perpMarketAccount.marketIndex,
185
- name: decodeName(perpMarketAccount.name),
186
- baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(),
187
- quoteAssetReserve: perpMarketAccount.amm.quoteAssetReserve.toString()
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("oracleData price: ", oracleData.price.toString());
196
- console.log("oracleData: ", JSON.stringify({
197
- price: oracleData.price.toString(),
198
- confidence: oracleData.confidence?.toString(),
199
- slot: oracleData.slot?.toString()
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(`🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}`);
215
- console.log(`🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${perpMarketIndexes.length} markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)`);
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
+ });
@@ -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 += 16;
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