diff --git a/src/abacus-ts/calculators/accountActions.ts b/src/abacus-ts/calculators/accountActions.ts new file mode 100644 index 000000000..499bb5e96 --- /dev/null +++ b/src/abacus-ts/calculators/accountActions.ts @@ -0,0 +1,174 @@ +import { produce } from 'immer'; + +import { + IndexerAssetPositionResponseObject, + IndexerPositionSide, +} from '@/types/indexer/indexerApiGen'; + +import { MustBigNumber } from '@/lib/numbers'; + +import { freshChildSubaccount, newUsdcAssetPosition } from '../lib/subaccountUtils'; +import { + ModifyUsdcAssetPositionProps, + SubaccountBatchedOperations, + SubaccountOperations, +} from '../types/operationTypes'; +import { ParentSubaccountData } from '../types/rawTypes'; + +function addUsdcAssetPosition( + parentSubaccount: ParentSubaccountData, + payload: Pick +): ParentSubaccountData { + const { side, size, subaccountNumber } = payload; + return produce(parentSubaccount, (draftParentSubaccountData) => { + let childSubaccount = draftParentSubaccountData.childSubaccounts[subaccountNumber]; + + if (childSubaccount == null) { + // Upsert ChildSubaccountData into parentSubaccountData.childSubaccounts + const updatedChildSubaccount = freshChildSubaccount({ + address: draftParentSubaccountData.address, + subaccountNumber, + }); + + childSubaccount = { + ...updatedChildSubaccount, + assetPositions: { + ...updatedChildSubaccount.assetPositions, + USDC: newUsdcAssetPosition({ + side, + size, + subaccountNumber, + }), + }, + }; + } else { + if (childSubaccount.assetPositions.USDC == null) { + // Upsert USDC Asset Position + childSubaccount.assetPositions.USDC = newUsdcAssetPosition({ + side, + size, + subaccountNumber, + }); + } else { + if (childSubaccount.assetPositions.USDC.side !== side) { + const signedSizeBN = MustBigNumber(childSubaccount.assetPositions.USDC.size).minus(size); + + if (signedSizeBN.lte(0)) { + // New size flips the Asset Position Side + childSubaccount.assetPositions.USDC.side = + side === IndexerPositionSide.LONG + ? IndexerPositionSide.SHORT + : IndexerPositionSide.LONG; + childSubaccount.assetPositions.USDC.size = signedSizeBN.abs().toString(); + } else { + // Set the new size of the Asset Position + childSubaccount.assetPositions.USDC.size = signedSizeBN.toString(); + } + } else { + // Side is maintained, add the size to the existing position + childSubaccount.assetPositions.USDC.size = MustBigNumber( + childSubaccount.assetPositions.USDC.size + ) + .plus(size) + .toString(); + } + } + } + }); +} + +export function createUsdcDepositOperations( + parentSubaccount: ParentSubaccountData, + { + subaccountNumber, + depositAmount, + }: { + subaccountNumber: number; + depositAmount: string; + } +): SubaccountBatchedOperations { + const updatedParentSubaccountData = addUsdcAssetPosition(parentSubaccount, { + side: IndexerPositionSide.LONG, + size: depositAmount, + subaccountNumber, + }); + + if (updatedParentSubaccountData.childSubaccounts[subaccountNumber]?.assetPositions.USDC == null) { + throw new Error('USDC Asset Position was improperly modified'); + } + + return { + operations: [ + SubaccountOperations.ModifyUsdcAssetPosition({ + subaccountNumber, + changes: updatedParentSubaccountData.childSubaccounts[subaccountNumber].assetPositions.USDC, + }), + ], + }; +} + +export function createUsdcWithdrawalOperations( + parentSubaccount: ParentSubaccountData, + { + subaccountNumber, + withdrawAmount, + }: { + subaccountNumber: number; + withdrawAmount: string; + } +): SubaccountBatchedOperations { + const updatedParentSubaccountData = addUsdcAssetPosition(parentSubaccount, { + side: IndexerPositionSide.SHORT, + size: withdrawAmount, + subaccountNumber, + }); + + if (updatedParentSubaccountData.childSubaccounts[subaccountNumber]?.assetPositions.USDC == null) { + throw new Error('USDC Asset Position was improperly modified'); + } + + return { + operations: [ + SubaccountOperations.ModifyUsdcAssetPosition({ + subaccountNumber, + changes: updatedParentSubaccountData.childSubaccounts[subaccountNumber].assetPositions.USDC, + }), + ], + }; +} + +function modifyUsdcAssetPosition( + parentSubaccountData: ParentSubaccountData, + payload: ModifyUsdcAssetPositionProps +): ParentSubaccountData { + const { subaccountNumber, changes } = payload; + + return produce(parentSubaccountData, (draftParentSubaccountData) => { + if (draftParentSubaccountData.childSubaccounts[subaccountNumber]?.assetPositions.USDC != null) { + draftParentSubaccountData.childSubaccounts[subaccountNumber].assetPositions.USDC = changes; + } + }); +} + +export function applyOperationsToSubaccount( + parentSubaccount: ParentSubaccountData, + batchedOperations: SubaccountBatchedOperations +): ParentSubaccountData { + let parentSubaccountData: ParentSubaccountData = parentSubaccount; + + batchedOperations.operations.forEach((op) => { + SubaccountOperations.match(op, { + AddPerpetualPosition: () => { + // TODO: Implement addPerpetualPosition + }, + ModifyPerpetualPosition: () => { + // TODO: Implement modifyPerpetualPosition + }, + ModifyUsdcAssetPosition: (args) => { + parentSubaccountData = modifyUsdcAssetPosition(parentSubaccountData, args); + }, + }); + }); + + return parentSubaccountData; +} diff --git a/src/abacus-ts/lib/subaccountUtils.ts b/src/abacus-ts/lib/subaccountUtils.ts new file mode 100644 index 000000000..868c07604 --- /dev/null +++ b/src/abacus-ts/lib/subaccountUtils.ts @@ -0,0 +1,60 @@ +import { + IndexerPositionSide, + IndexerSubaccountResponseObject, +} from '@/types/indexer/indexerApiGen'; + +import { ChildSubaccountData } from '../types/rawTypes'; + +export function isValidSubaccount(childSubaccount: IndexerSubaccountResponseObject) { + return ( + Object.keys(childSubaccount.assetPositions).length > 0 || + Object.keys(childSubaccount.openPerpetualPositions).length > 0 + ); +} + +export function convertToStoredChildSubaccount({ + address, + subaccountNumber, + assetPositions, + openPerpetualPositions, +}: IndexerSubaccountResponseObject): ChildSubaccountData { + return { + address, + subaccountNumber, + assetPositions, + openPerpetualPositions, + }; +} + +export function freshChildSubaccount({ + address, + subaccountNumber, +}: { + address: string; + subaccountNumber: number; +}): ChildSubaccountData { + return { + address, + subaccountNumber, + assetPositions: {}, + openPerpetualPositions: {}, + }; +} + +export function newUsdcAssetPosition({ + side, + size, + subaccountNumber, +}: { + side: IndexerPositionSide; + size: string; + subaccountNumber: number; +}) { + return { + assetId: '0', + size, + subaccountNumber, + side, + symbol: 'USDC', + }; +} diff --git a/src/abacus-ts/ontology.ts b/src/abacus-ts/ontology.ts index ba00a732a..5b8bfcfcb 100644 --- a/src/abacus-ts/ontology.ts +++ b/src/abacus-ts/ontology.ts @@ -17,6 +17,10 @@ import { selectParentSubaccountSummaryLoading, selectUnopenedIsolatedPositions, } from './selectors/account'; +import { + createSelectParentSubaccountSummaryDeposit, + createSelectParentSubaccountSummaryWithdrawal, +} from './selectors/accountActions'; import { selectAllAssetsInfo, selectAllAssetsInfoLoading, @@ -98,6 +102,14 @@ export const BonsaiHelpers = { fills: getCurrentMarketAccountFills, }, }, + forms: { + deposit: { + createSelectParentSubaccountSummary: createSelectParentSubaccountSummaryDeposit, + }, + withdraw: { + createSelectParentSubaccountSummary: createSelectParentSubaccountSummaryWithdrawal, + }, + }, unopenedIsolatedPositions: selectUnopenedIsolatedPositions, } as const satisfies NestedSelectors; diff --git a/src/abacus-ts/selectors/account.ts b/src/abacus-ts/selectors/account.ts index b3196519c..7d025b4db 100644 --- a/src/abacus-ts/selectors/account.ts +++ b/src/abacus-ts/selectors/account.ts @@ -54,7 +54,7 @@ const selectRelevantMarketsList = createAppSelector( } ); -const selectRelevantMarketsData = createAppSelector( +export const selectRelevantMarketsData = createAppSelector( [selectRelevantMarketsList, selectRawMarketsData], (marketIds, markets) => { if (markets == null || marketIds == null) { diff --git a/src/abacus-ts/selectors/accountActions.ts b/src/abacus-ts/selectors/accountActions.ts new file mode 100644 index 000000000..5ea144373 --- /dev/null +++ b/src/abacus-ts/selectors/accountActions.ts @@ -0,0 +1,60 @@ +import { createAppSelector } from '@/state/appTypes'; + +import { + applyOperationsToSubaccount, + createUsdcDepositOperations, + createUsdcWithdrawalOperations, +} from '../calculators/accountActions'; +import { calculateParentSubaccountSummary } from '../calculators/subaccount'; +import { selectRelevantMarketsData } from './account'; +import { selectRawParentSubaccountData } from './base'; + +export const createSelectParentSubaccountSummaryDeposit = () => + createAppSelector( + [ + selectRawParentSubaccountData, + selectRelevantMarketsData, + ( + _s, + input: { + subaccountNumber: number; + depositAmount: string; + } + ) => input, + ], + (parentSubaccount, markets, depositInputs) => { + if (parentSubaccount == null || markets == null) { + return undefined; + } + + const operations = createUsdcDepositOperations(parentSubaccount, depositInputs); + const modifiedParentSubaccount = applyOperationsToSubaccount(parentSubaccount, operations); + const result = calculateParentSubaccountSummary(modifiedParentSubaccount, markets); + return result; + } + ); + +export const createSelectParentSubaccountSummaryWithdrawal = () => + createAppSelector( + [ + selectRawParentSubaccountData, + selectRelevantMarketsData, + ( + _s, + input: { + subaccountNumber: number; + withdrawAmount: string; + } + ) => input, + ], + (parentSubaccount, markets, withdrawalInputs) => { + if (parentSubaccount == null || markets == null) { + return undefined; + } + + const operations = createUsdcWithdrawalOperations(parentSubaccount, withdrawalInputs); + const modifiedParentSubaccount = applyOperationsToSubaccount(parentSubaccount, operations); + const result = calculateParentSubaccountSummary(modifiedParentSubaccount, markets); + return result; + } + ); diff --git a/src/abacus-ts/types/operationTypes.ts b/src/abacus-ts/types/operationTypes.ts index c8ef0a190..f691ea630 100644 --- a/src/abacus-ts/types/operationTypes.ts +++ b/src/abacus-ts/types/operationTypes.ts @@ -5,22 +5,26 @@ import { IndexerPerpetualPositionResponseObject, } from '@/types/indexer/indexerApiGen'; +export type AddPerpetualPositionProps = { + subaccountNumber: number; + market: string; + changes: Omit; +}; + +export type ModifyPerpetualPositionProps = { + changes: Partial>; +}; + +export type ModifyUsdcAssetPositionProps = { + subaccountNumber: number; + changes: IndexerAssetPositionResponseObject; +}; + export const SubaccountOperations = unionize( { - AddPerpetualPosition: ofType<{ - subaccountNumber: string; - market: string; - position: Omit; - }>(), - ModifyPerpetualPosition: ofType<{ - subaccountNumber: string; - market: string; - changes: Partial>; - }>(), - ModifyUsdcAssetPosition: ofType<{ - subaccountNumber: string; - changes: Partial>; - }>(), + AddPerpetualPosition: ofType(), + ModifyPerpetualPosition: ofType(), + ModifyUsdcAssetPosition: ofType(), }, { tag: 'operation' as const, value: 'payload' as const } ); diff --git a/src/abacus-ts/websocket/parentSubaccount.ts b/src/abacus-ts/websocket/parentSubaccount.ts index d19eb9a25..851279c86 100644 --- a/src/abacus-ts/websocket/parentSubaccount.ts +++ b/src/abacus-ts/websocket/parentSubaccount.ts @@ -5,7 +5,6 @@ import { IndexerAssetPositionResponseObject, IndexerOrderResponseObject, IndexerPerpetualPositionResponseObject, - IndexerSubaccountResponseObject, } from '@/types/indexer/indexerApiGen'; import { isWsParentSubaccountSubscribed, @@ -23,49 +22,18 @@ import { MustBigNumber } from '@/lib/numbers'; import { accountRefreshSignal } from '../accountRefreshSignal'; import { createStoreEffect } from '../lib/createStoreEffect'; import { Loadable, loadableIdle, loadableLoaded, loadablePending } from '../lib/loadable'; +import { + convertToStoredChildSubaccount, + freshChildSubaccount, + isValidSubaccount, +} from '../lib/subaccountUtils'; import { logAbacusTsError } from '../logs'; import { selectParentSubaccountInfo, selectWebsocketUrl } from '../socketSelectors'; -import { ChildSubaccountData, ParentSubaccountData } from '../types/rawTypes'; +import { ParentSubaccountData } from '../types/rawTypes'; import { makeWsValueManager, subscribeToWsValue } from './lib/indexerValueManagerHelpers'; import { IndexerWebsocket } from './lib/indexerWebsocket'; import { WebsocketDerivedValue } from './lib/websocketDerivedValue'; -function isValidSubaccount(childSubaccount: IndexerSubaccountResponseObject) { - return ( - Object.keys(childSubaccount.assetPositions).length > 0 || - Object.keys(childSubaccount.openPerpetualPositions).length > 0 - ); -} - -function convertToStoredChildSubaccount({ - address, - subaccountNumber, - assetPositions, - openPerpetualPositions, -}: IndexerSubaccountResponseObject): ChildSubaccountData { - return { - address, - subaccountNumber, - assetPositions, - openPerpetualPositions, - }; -} - -function freshChildSubaccount({ - address, - subaccountNumber, -}: { - address: string; - subaccountNumber: number; -}): ChildSubaccountData { - return { - address, - subaccountNumber, - assetPositions: {}, - openPerpetualPositions: {}, - }; -} - interface AccountValueArgsBase { address: string; parentSubaccountNumber: string; diff --git a/src/views/dialogs/DepositDialog2/queries.ts b/src/views/dialogs/DepositDialog2/queries.ts index 6d906a985..e0e295121 100644 --- a/src/views/dialogs/DepositDialog2/queries.ts +++ b/src/views/dialogs/DepositDialog2/queries.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { BonsaiHelpers } from '@/abacus-ts/ontology'; import { BalanceRequest, RouteRequest, SkipClient } from '@skip-go/client'; import { useQuery } from '@tanstack/react-query'; import { Chain, parseUnits } from 'viem'; @@ -14,6 +15,7 @@ import { WalletNetworkType } from '@/constants/wallets'; import { useSkipClient } from '@/hooks/transfers/skipClient'; import { useAccounts } from '@/hooks/useAccounts'; +import { useParameterizedSelector } from '@/hooks/useParameterizedSelector'; import { SourceAccount } from '@/state/wallet'; @@ -147,3 +149,20 @@ export function useDepositRoutes(token: TokenForTransfer, amount: string) { placeholderData: (prev) => prev, }); } + +export function useDepositDeltas({ depositAmount }: { depositAmount: string }) { + const depositInput = useMemo( + () => ({ + subaccountNumber: 0, + depositAmount, + }), + [depositAmount] + ); + + const modifiedParentSubaccount = useParameterizedSelector( + BonsaiHelpers.forms.deposit.createSelectParentSubaccountSummary, + depositInput + ); + + return modifiedParentSubaccount; +} diff --git a/src/views/dialogs/WithdrawDialog2/queries.ts b/src/views/dialogs/WithdrawDialog2/queries.ts index 6e7fce80e..01642df9e 100644 --- a/src/views/dialogs/WithdrawDialog2/queries.ts +++ b/src/views/dialogs/WithdrawDialog2/queries.ts @@ -1,3 +1,6 @@ +import { useMemo } from 'react'; + +import { BonsaiHelpers } from '@/abacus-ts/ontology'; import { RouteRequest, SkipClient } from '@skip-go/client'; import { useQuery } from '@tanstack/react-query'; import { parseUnits } from 'viem'; @@ -7,6 +10,7 @@ import { timeUnits } from '@/constants/time'; import { DYDX_CHAIN_USDC_DENOM, TokenForTransfer } from '@/constants/tokens'; import { useSkipClient } from '@/hooks/transfers/skipClient'; +import { useParameterizedSelector } from '@/hooks/useParameterizedSelector'; async function getSkipWithdrawalRoutes( skipClient: SkipClient, @@ -53,3 +57,20 @@ export function useWithdrawalRoutes({ retry: false, }); } + +export function useWithdrawalDeltas({ withdrawAmount }: { withdrawAmount: string }) { + const withdrawInput = useMemo( + () => ({ + subaccountNumber: 0, + withdrawAmount, + }), + [withdrawAmount] + ); + + const modifiedParentSubaccount = useParameterizedSelector( + BonsaiHelpers.forms.withdraw.createSelectParentSubaccountSummary, + withdrawInput + ); + + return modifiedParentSubaccount; +}