Skip to content

Commit 68edfec

Browse files
LukasDecocrispheaneylowkeyniccgithub-actions[bot]0xbigz
authored
lukas/isolated positions sdk (#1965)
* program: make lp shares reduce only * init * rm more fields * make tests build * start sdk changes * init new margin calc * deposit and transfer into * add settle pnl * program: add withdraw * add more ix * add new meets withdraw req fn * enter/exit liquidation logic * moar * start liquidation logic * other liquidation fns * make build work * more updates * always calc isolated pos * rm isolated position market index logic * moar * program: rm the isolated position market index * some tweaks * rm some old margin code * tweak meets withdraw requirements * rm liquidation mode changing context * handle liquidation id and bit flags * more liquidation changes * clean * fix force cancel orders * update validate liquidation * moar * rename is_being_liquidated * start adding test * program: add validate for liq borrow for perp pnl * program: add test for isolated margin calc * is bankrupt test * fix cancel orders * fix set liquidation status * more tweaks * clean up naming * update last active slot for isolated position liq * another liquidation review * add test * cargo fmt -- * tweak naming * add test to make sure false liquidaiton wont be triggered * test meets withdraw * change is bankrupt * more * update uses of exit isolated liquidaiton * moar * moar * reduce diff * moar * modularize some for tests * add tests for the pnl for deposit liquidation * tests for isolated position transfer * test for update spot balance * test for settle pnl * add perp position max margin * program: test for custom perp position margin ratio * add test for margin calc for disable hlm * update test name * make max margin ratio persist * add liquidation mode test * more tests to make sure liqudiations dont bleed over * change test name * fix broken cargo tests * cargo fmt -- * first ts test * isolatedPositionLiquidatePerp test * isolatedPositionLiquidatePerpwithFill test * fix expired position * cargo fmt -- * feat: initial SDK Changes for iso pos * feat: margin calc unit tests * temp * feat: finally - parity with on-chain cargo test * fix: PR feedback and cleanup + decoding position flag wrong * feat: deposit into iso position ixs * temp: pr feedback nother round * feat: per perp pos max margin ratio * feat: additional ixs for transfer into iso + update perp margin ratio * feat: revamp liquidation checker functions for cross vs iso margin * fix: adjust health getter for user * fix: liq statuses add to return signature * chore: post rebase cleaner upper * fix: missing params from per market lev * feat: zero out account withdraw from perp position * fix: available positions logic update for iso * feat: iso position txs cleanup + ix ordering * feat: onchain props for signed msg orders + idl update * feat: cancels withdraw from iso pos * fix: only settle if needed iso withdraw + i64 min * feat: improvements to single grpc test * feat: buffer on margin deposits to avoid insuff collat err * feat: helpful scripts for testing/manipulating iso positions * chore: re organizing some user sdk funcs * fix: bug with max amount withdrawal for transfer iso perp * fix: post merge dupe field on swift * feat: min and max 64 constants * fix: bug with margin removal * fix: missing swift iso deposit from idl * fix: lint and prettify * feat: increased buffer on isolated deposit opening position * fix: missing check on order increasing size for depositing margin place + take * feat: settle pnl when trying to transfer to cross * fix: incorrect iso bankruptcy flag * feat: new margin calc logic * fix: broken test helpers * feat: buffer adjustments * fix: add missing swap ix update * fix: undefined this on isoalted free collateral * fix: prettier broke * fix: handle perp buying power on new iso position * fix: try/catch wrap on user isolated get free collat * feat: try settle flag for transfer to iso perp * fix: properly handle perp buying power existing iso position increase * feat: alpha npm version * fix: decoding isolated scaled balance incorrectly maybe * fix: build error rm lp field * feat: publish isPerpPositionIsolated as public method * sdk: add ix for token 2022 init account deposits (#2050) * add ix for token 2022 init account deposits * only call checkAccountExists if token2022 * update changelog * sdk: release v2.152.0-beta.3 * program: add bit_flags in preparation for iso pos (#2053) * program: add bit_flags in preparation for iso pos * CHANGELOG * program: base-spread-validate-buffer (#2052) * program: base-spread-validate-buffer * CHANGELOG --------- Co-authored-by: Chris Heaney <chrisheaney30@gmail.com> * v2.152.0 * ui: save titan tx when quoted and reuse on swap (#2055) * fix titan quoting for dsol * fix dsol instant unstake * feat: minified with esbuild (#2056) * feat: minified with esbuild * fix: rm webpack * fix: prettier titanClient * ui: fix falsely failing quotes from titan (#2058) * ui: fix falsely failing quotes from titan * prettify * check feed id after pyth pull atomic update * fix: many null checks fixed (#2059) * fix: many null checks fixed * fix: prettier * feat: get active markets helpers * fix: prettier * fix: README on margin calc was incorrect * fix: prettier and lint * fix: rm weird program diff * fix: rm weird program diff 2 * fix: rm weird empty file = * fix: rm useless diffs * refactor: remove unused fields a bunch on margin calc * fix: rebuild idl * fix: prevent unnecessary excpetion on liq price if free collat delta not computed * program: delete sereum/ob configs ix (#2066) * hot wallet update config stats * add delete for serum * delete ob config * CHANGELOG * sdk: release v2.153.0-beta.2 * Nick/external init account support (#2067) * wip: add external signer support for init account, stats account, swift account * updates * fix prettify * sdk: release v2.153.0-beta.3 * fix: user fastDecode check iso deposit and pos flag * fix: broken tests and PR cleanup * fix: final anchor tests fix? --------- Co-authored-by: Chris Heaney <chrisheaney30@gmail.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: wphan <6348407+wphan@users.noreply.github.com> Co-authored-by: chakos <chrishakos@lunoho.company>
1 parent 904a87e commit 68edfec

19 files changed

+1106
-153
lines changed

sdk/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.155.0-beta.4
1+
2.155.0-beta.4
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
2+
import dotenv from 'dotenv';
3+
import { AnchorProvider, Idl, Program, ProgramAccount, BN } from '@coral-xyz/anchor';
4+
import driftIDL from '../src/idl/drift.json';
5+
import {
6+
DRIFT_PROGRAM_ID,
7+
PerpMarketAccount,
8+
SpotMarketAccount,
9+
OracleInfo,
10+
Wallet,
11+
numberToSafeBN,
12+
} from '../src';
13+
import { DriftClient } from '../src/driftClient';
14+
import { DriftClientConfig } from '../src/driftClientConfig';
15+
16+
async function main() {
17+
dotenv.config({ path: '../' });
18+
19+
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
20+
if (!RPC_ENDPOINT) throw new Error('RPC_ENDPOINT env var required');
21+
22+
let keypair: Keypair;
23+
const pk = process.env.PRIVATE_KEY;
24+
if (pk) {
25+
const secret = Uint8Array.from(JSON.parse(pk));
26+
keypair = Keypair.fromSecretKey(secret);
27+
} else {
28+
keypair = new Keypair();
29+
console.warn('Using ephemeral keypair. Provide PRIVATE_KEY to use a real wallet.');
30+
}
31+
const wallet = new Wallet(keypair);
32+
33+
const connection = new Connection(RPC_ENDPOINT);
34+
const provider = new AnchorProvider(connection, wallet as any, {
35+
commitment: 'processed',
36+
});
37+
const programId = new PublicKey(DRIFT_PROGRAM_ID);
38+
const program = new Program(driftIDL as Idl, programId, provider);
39+
40+
const allPerpMarketProgramAccounts =
41+
(await program.account.perpMarket.all()) as ProgramAccount<PerpMarketAccount>[];
42+
const perpMarketIndexes = allPerpMarketProgramAccounts.map((val) => val.account.marketIndex);
43+
const allSpotMarketProgramAccounts =
44+
(await program.account.spotMarket.all()) as ProgramAccount<SpotMarketAccount>[];
45+
const spotMarketIndexes = allSpotMarketProgramAccounts.map((val) => val.account.marketIndex);
46+
47+
const seen = new Set<string>();
48+
const oracleInfos: OracleInfo[] = [];
49+
for (const acct of allPerpMarketProgramAccounts) {
50+
const key = `${acct.account.amm.oracle.toBase58()}-${Object.keys(acct.account.amm.oracleSource)[0]}`;
51+
if (!seen.has(key)) {
52+
seen.add(key);
53+
oracleInfos.push({ publicKey: acct.account.amm.oracle, source: acct.account.amm.oracleSource });
54+
}
55+
}
56+
for (const acct of allSpotMarketProgramAccounts) {
57+
const key = `${acct.account.oracle.toBase58()}-${Object.keys(acct.account.oracleSource)[0]}`;
58+
if (!seen.has(key)) {
59+
seen.add(key);
60+
oracleInfos.push({ publicKey: acct.account.oracle, source: acct.account.oracleSource });
61+
}
62+
}
63+
64+
const clientConfig: DriftClientConfig = {
65+
connection,
66+
wallet,
67+
programID: programId,
68+
accountSubscription: { type: 'websocket', commitment: 'processed' },
69+
perpMarketIndexes,
70+
spotMarketIndexes,
71+
oracleInfos,
72+
env: 'devnet',
73+
};
74+
const client = new DriftClient(clientConfig);
75+
await client.subscribe();
76+
77+
const candidates = perpMarketIndexes.filter((i) => i >= 0 && i <= 5);
78+
const targetMarketIndex = candidates.length
79+
? candidates[Math.floor(Math.random() * candidates.length)]
80+
: perpMarketIndexes[0];
81+
82+
const perpMarketAccount = client.getPerpMarketAccount(targetMarketIndex);
83+
const quoteSpotMarketIndex = perpMarketAccount.quoteSpotMarketIndex;
84+
const spotMarketAccount = client.getSpotMarketAccount(quoteSpotMarketIndex);
85+
86+
const precision = new BN(10).pow(new BN(spotMarketAccount.decimals));
87+
const amount = numberToSafeBN(0.01, precision);
88+
89+
const userTokenAccount = await client.getAssociatedTokenAccount(quoteSpotMarketIndex);
90+
const ix = await client.getDepositIntoIsolatedPerpPositionIx(
91+
amount,
92+
targetMarketIndex,
93+
userTokenAccount,
94+
0
95+
);
96+
97+
const tx = await client.buildTransaction([ix]);
98+
const { txSig } = await client.sendTransaction(tx);
99+
console.log(`Deposited into isolated perp market ${targetMarketIndex}: ${txSig}`);
100+
101+
await client.getUser().unsubscribe();
102+
await client.unsubscribe();
103+
}
104+
105+
main().catch((e) => {
106+
console.error(e);
107+
process.exit(1);
108+
});
109+
110+

sdk/scripts/find-flagged-users.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

0 commit comments

Comments
 (0)