Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib: live position & oracle price updates v1 #577

Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/AdrenaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4036,12 +4036,15 @@ export class AdrenaClient {

// Positions PDA can be found by deriving each mints supported by the pool for 2 sides
// DO NOT LOAD PNL OR LIQUIDATION PRICE
public async loadUserPositions(user: PublicKey): Promise<PositionExtended[]> {
const possiblePositionAddresses = this.getPossiblePositionAddresses(user);

public async loadUserPositions(
user: PublicKey,
positionAddresses?: Array<PublicKey>,
): Promise<PositionExtended[]> {
const actualPositionAddresses =
positionAddresses || this.getPossiblePositionAddresses(user);
const positions =
(await this.readonlyAdrenaProgram.account.position.fetchMultiple(
possiblePositionAddresses,
actualPositionAddresses,
'recent',
)) as (Position | null)[];

Expand All @@ -4054,14 +4057,15 @@ export class AdrenaClient {

const positionExtended = this.extendPosition(
position,
possiblePositionAddresses[index],
actualPositionAddresses[index],
);

if (!positionExtended) {
if (positionExtended) {
acc.push(positionExtended);
return acc;
}

return [...acc, positionExtended];
return acc;
},
[] as PositionExtended[],
);
Expand Down
7 changes: 7 additions & 0 deletions src/actions/streamingTokenPrices.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const SET_STREAMING_TOKEN_PRICE_ACTION_TYPE =
'setStreamingTokenPrice' as const;

export const STOP_STREAMING_TOKEN_PRICES_ACTION_TYPE =
'stopStreamingTokenPrice' as const;

export const setStreamingTokenPrice = (
tokenSymbol: string,
price: number | null,
Expand All @@ -11,3 +14,7 @@ export const setStreamingTokenPrice = (
price,
},
});

export const stopStreamingTokenPrices = () => ({
type: STOP_STREAMING_TOKEN_PRICES_ACTION_TYPE,
});
188 changes: 187 additions & 1 deletion src/actions/thunks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { BN } from '@coral-xyz/anchor';
import { NATIVE_MINT } from '@solana/spl-token';
import type { AccountInfo, PublicKey } from '@solana/web3.js';

import { SOL_DECIMALS } from '@/constant';
import type { TokenPricesState } from '@/reducers/streamingTokenPricesReducer';
import { selectPossibleUserPositions } from '@/selectors/positions';
import { selectStreamingTokenPricesFallback } from '@/selectors/streamingTokenPrices';
import { selectWalletPublicKey } from '@/selectors/wallet';
import type { Dispatch, RootState } from '@/store/store';
import type { TokenSymbol } from '@/types';
import type { PositionExtended, TokenSymbol } from '@/types';
import { findATAAddressSync, nativeToUi } from '@/utils';

import { setWalletTokenBalances } from './walletBalances';
Expand Down Expand Up @@ -69,3 +73,185 @@ export const fetchWalletTokenBalances =
),
);
};

export const calculatePnLandLiquidationPrice = (
position: PositionExtended,
tokenPrices: TokenPricesState,
) => {
const pnl = window.adrena.client.calculatePositionPnL({
position,
tokenPrices,
});

if (pnl === null) {
return;
}

const { profitUsd, lossUsd, borrowFeeUsd } = pnl;

position.profitUsd = profitUsd;
position.lossUsd = lossUsd;
position.borrowFeeUsd = borrowFeeUsd;
position.pnl = profitUsd + -lossUsd;
position.pnlMinusFees = position.pnl + borrowFeeUsd + position.exitFeeUsd;
position.currentLeverage =
position.sizeUsd / (position.collateralUsd + position.pnl);

// Calculate liquidation price
const liquidationPrice = window.adrena.client.calculateLiquidationPrice({
position,
});

if (liquidationPrice !== null) {
position.liquidationPrice = liquidationPrice;
}
0xcryptovenom marked this conversation as resolved.
Show resolved Hide resolved
};

export const fetchUserPositions =
() =>
async (
dispatch: Dispatch,
getState: () => RootState,
): Promise<Array<PositionExtended> | null> => {
const connection = window.adrena.client.readonlyConnection;
const walletPublicKey = selectWalletPublicKey(getState());
const possibleUserPositions = selectPossibleUserPositions(getState());

if (!connection || !walletPublicKey || !possibleUserPositions) {
return null;
}

try {
const positions = await window.adrena.client.loadUserPositions(
walletPublicKey,
possibleUserPositions,
);

const tokenPrices = selectStreamingTokenPricesFallback(getState());
for (const position of positions) {
try {
calculatePnLandLiquidationPrice(position, tokenPrices);
} catch (err) {
console.error(
'Unexpected error calculating PnL / liquidation price',
err,
);
}
}

return positions;
} catch (err) {
console.error('Unexpected error fetching user positions', err);

return null;
}
};

let subscribed = false;
export const subscribeToUserPositions =
({
onPositionUpdated,
onPositionDeleted,
}: {
onPositionUpdated: (position: PositionExtended) => unknown;
onPositionDeleted: (userPosition: PublicKey) => unknown;
}) =>
(dispatch: Dispatch, getState: () => RootState) => {
const connection = window.adrena.client.connection;
const walletPublicKey = selectWalletPublicKey(getState());
const possibleUserPositions = selectPossibleUserPositions(getState());

if (!connection || !walletPublicKey || !possibleUserPositions) {
return;
}

// Ensuring we only subscribe once to user position changes.
if (subscribed) {
return;
}

const subscriptions = new Map<number, PublicKey>();
const removeSubscription = (subscriptionId: number) => {
connection.removeAccountChangeListener(subscriptionId);
subscriptions.delete(subscriptionId);
};
const unsubscribe = () => {
subscribed = false;
for (const [subscriptionId] of subscriptions) {
removeSubscription(subscriptionId);
}
};

for (const possibleUserPosition of possibleUserPositions) {
const subscriptionId = connection.onAccountChange(
0xcryptovenom marked this conversation as resolved.
Show resolved Hide resolved
possibleUserPosition,
(accountInfo: AccountInfo<Buffer>) => {
const userPosition = possibleUserPosition;

// Position got deleted
if (!accountInfo.data.length) {
onPositionDeleted(userPosition);
return;
}

try {
const positionData = window.adrena.client
.getReadonlyAdrenaProgram()
.coder.accounts.decode('position', accountInfo.data);
const extendedPosition = window.adrena.client.extendPosition(
positionData,
userPosition,
);

if (!extendedPosition) {
throw new Error('Unexpected null extended user position');
}

onPositionUpdated(extendedPosition);
} catch (err) {
console.error(
'Unexpected error decoding / extending user position',
err,
);
}
},
);
subscriptions.set(subscriptionId, possibleUserPosition);
}

return unsubscribe;
};

export const fetchAndSubscribeToFullUserPositions =
(
onPositionUpdated: (position: PositionExtended) => unknown,
0xcryptovenom marked this conversation as resolved.
Show resolved Hide resolved
onPositionDeleted: (userPosition: PublicKey) => unknown,
) =>
(dispatch: Dispatch, getState: () => RootState) => {
// Fetch the initial user positions
// keep the promise to be returned synchronously, do not await it.
const positionsPromise = dispatch(fetchUserPositions());

const unsubscribe = dispatch(
subscribeToUserPositions({
onPositionUpdated: function augmentPositionWithPnL(position) {
const tokenPrices = selectStreamingTokenPricesFallback(getState());
try {
// PositionExtended objects are augmented in place.
calculatePnLandLiquidationPrice(position, tokenPrices);
} catch (err) {
console.error(
'Unexpected error calculating PnL / liquidation price',
err,
);
}
onPositionUpdated(position);
},
onPositionDeleted,
}),
);

// We must return the unsubscribe function synchronously
// so effect-based react component / hook consumer can clean-up.
return [positionsPromise, unsubscribe] as const;
};
Loading