Skip to content

Commit

Permalink
Show net funding and position value (perpetual only) (#530)
Browse files Browse the repository at this point in the history
* Show net funding and position value (perpetual only)

* FE changes

* fix format

* fix

* move position value to user profile

* fix

* fix

* Fix formatting, rename column

---------

Co-authored-by: Tomasz Tórz <[email protected]>
  • Loading branch information
adamiak and torztomasz authored Oct 29, 2024
1 parent e9d06f7 commit 5275df7
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 20 deletions.
4 changes: 3 additions & 1 deletion packages/backend/src/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,9 @@ export class Application {
preprocessedUserL2TransactionsStatisticsRepository,
vaultRepository,
config.starkex.l2Transactions.excludeTypes,
config.starkex.contracts.perpetual
config.starkex.contracts.perpetual,
stateUpdater,
stateUpdateRepository
)
const stateUpdateController = new StateUpdateController(
pageContextService,
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/api/controllers/EscapeHatchController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {
PageContextWithUser,
UserDetails,
} from '@explorer/shared'
import { MerkleProof, PositionLeaf } from '@explorer/state'
import { EthereumAddress } from '@explorer/types'

import { FreezeCheckService } from '../../core/FreezeCheckService'
import { PageContextService } from '../../core/PageContextService'
import { StateUpdater } from '../../core/StateUpdater'
import { StateUpdateRepository } from '../../peripherals/database/StateUpdateRepository'
import { UserTransactionRecord } from '../../peripherals/database/transactions/UserTransactionRepository'
import { calculatePositionValue } from '../../utils/calculatePositionValue'
import { ControllerResult } from './ControllerResult'
import { serializeMerkleProofForEscape } from './serializeMerkleProofForEscape'

Expand Down Expand Up @@ -157,6 +159,13 @@ export class EscapeHatchController {
const serializedState = encodeStateAsInt256Array(
latestStateUpdate.perpetualState
)
const positionValues =
context.tradingMode === 'perpetual'
? calculatePositionValue(
merkleProof as MerkleProof<PositionLeaf>,
latestStateUpdate.perpetualState
)
: undefined
let content: string
switch (context.tradingMode) {
case 'perpetual':
Expand All @@ -166,6 +175,9 @@ export class EscapeHatchController {
starkKey: merkleProof.starkKey,
escapeVerifierAddress: this.escapeVerifierAddress,
positionOrVaultId,
positionValue: positionValues?.positionValue
? positionValues.positionValue / 10000n
: undefined,
serializedMerkleProof,
assetCount: merkleProof.perpetualAssetCount,
serializedState,
Expand All @@ -177,6 +189,7 @@ export class EscapeHatchController {
tradingMode: context.tradingMode,
starkKey: merkleProof.starkKey,
escapeVerifierAddress: this.escapeVerifierAddress,
positionValue: undefined,
positionOrVaultId,
serializedEscapeProof: serializedMerkleProof,
})
Expand Down
100 changes: 87 additions & 13 deletions packages/backend/src/api/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import {
TradingMode,
UserDetails,
} from '@explorer/shared'
import { MerkleProof, PositionLeaf } from '@explorer/state'
import { AssetHash, AssetId, EthereumAddress, StarkKey } from '@explorer/types'

import { L2TransactionTypesToExclude } from '../../config/starkex/StarkexConfig'
import { AssetDetailsMap } from '../../core/AssetDetailsMap'
import { AssetDetailsService } from '../../core/AssetDetailsService'
import { ForcedTradeOfferViewService } from '../../core/ForcedTradeOfferViewService'
import { PageContextService } from '../../core/PageContextService'
import { StateUpdater } from '../../core/StateUpdater'
import { PaginationOptions } from '../../model/PaginationOptions'
import { ForcedTradeOfferRepository } from '../../peripherals/database/ForcedTradeOfferRepository'
import { L2TransactionRepository } from '../../peripherals/database/L2TransactionRepository'
Expand All @@ -41,6 +43,7 @@ import {
PricesRecord,
PricesRepository,
} from '../../peripherals/database/PricesRepository'
import { StateUpdateRepository } from '../../peripherals/database/StateUpdateRepository'
import {
SentTransactionRecord,
SentTransactionRepository,
Expand All @@ -53,6 +56,10 @@ import { UserRegistrationEventRepository } from '../../peripherals/database/User
import { VaultRepository } from '../../peripherals/database/VaultRepository'
import { WithdrawableAssetRepository } from '../../peripherals/database/WithdrawableAssetRepository'
import { getAssetValueUSDCents } from '../../utils/assets'
import {
calculatePositionValue,
PositionValue,
} from '../../utils/calculatePositionValue'
import { ControllerResult } from './ControllerResult'
import { getCollateralAssetDetails } from './getCollateralAssetDetails'
import { EscapableMap, getEscapableAssets } from './getEscapableAssets'
Expand All @@ -79,7 +86,9 @@ export class UserController {
private readonly excludeL2TransactionTypes:
| L2TransactionTypesToExclude
| undefined,
private readonly exchangeAddress: EthereumAddress
private readonly exchangeAddress: EthereumAddress,
private readonly stateUpdater: StateUpdater,
private readonly stateUpdateRepository: StateUpdateRepository
) {}

async getUserRegisterPage(
Expand Down Expand Up @@ -248,13 +257,35 @@ export class UserController {
escapableAssetHashes,
})

// If escape process has started on perpetuals, hide all assets
const hideAllAssets =
context.freezeStatus === 'frozen' &&
context.tradingMode === 'perpetual' &&
escapableAssetHashes.length > 0

let positionValues: PositionValue | undefined
if (
!hideAllAssets &&
context.tradingMode === 'perpetual' &&
userAssets.length > 0
) {
const firstAsset = userAssets[0]
if (firstAsset !== undefined) {
positionValues = await this.getPositionValue(
firstAsset.positionOrVaultId,
context
)
}
}

const assetEntries = userAssets.map((a) =>
toUserAssetEntry(
a,
context.tradingMode,
escapableMap,
context.freezeStatus,
assetPrices,
positionValues,
collateralAsset?.assetId,
assetDetailsMap
)
Expand Down Expand Up @@ -287,12 +318,6 @@ export class UserController {
this.excludeL2TransactionTypes
)

// If escape process has started on perpetuals, hide all assets
const hideAllAssets =
context.freezeStatus === 'frozen' &&
context.tradingMode === 'perpetual' &&
escapableAssetHashes.length > 0

const content = renderUserPage({
context,
starkKey,
Expand All @@ -318,6 +343,9 @@ export class UserController {
this.forcedTradeOfferViewService.toFinalizableOfferEntry(offer)
),
assets: hideAllAssets ? [] : assetEntries, // When frozen and escaped, don't show assets
positionValue: positionValues?.positionValue
? positionValues.positionValue / 10000n
: undefined,
totalAssets: hideAllAssets ? 0 : userStatistics?.assetCount ?? 0,
balanceChanges: balanceChangesEntries,
totalBalanceChanges: userStatistics?.balanceChangeCount ?? 0,
Expand All @@ -331,6 +359,32 @@ export class UserController {
return { type: 'success', content }
}

async getPositionValue(positionOrVaultId: bigint, context: PageContext) {
if (context.tradingMode !== 'perpetual') {
return { fundingPayments: {}, positionValue: undefined }
}

const merkleProof = await this.stateUpdater.generateMerkleProof(
positionOrVaultId
)

const latestStateUpdate = await this.stateUpdateRepository.findLast()
if (!latestStateUpdate) {
throw new Error('No state update found')
}
if (!latestStateUpdate.perpetualState) {
throw new Error('No perpetual state found')
}

if (!(merkleProof.leaf instanceof PositionLeaf)) {
throw new Error('Merkle proof is not for a position')
}
return calculatePositionValue(
merkleProof as MerkleProof<PositionLeaf>,
latestStateUpdate.perpetualState
)
}

async getUserAssetsPage(
givenUser: Partial<UserDetails>,
starkKey: StarkKey,
Expand Down Expand Up @@ -377,23 +431,39 @@ export class UserController {
escapableAssetHashes,
})

const hideAllAssets =
context.freezeStatus === 'frozen' &&
context.tradingMode === 'perpetual' &&
escapableAssetHashes.length > 0

let postionValues: PositionValue | undefined
if (
!hideAllAssets &&
context.tradingMode === 'perpetual' &&
userAssets.length > 0
) {
const firstAsset = userAssets[0]
if (firstAsset !== undefined) {
postionValues = await this.getPositionValue(
firstAsset.positionOrVaultId,
context
)
}
}

const assets = userAssets.map((a) =>
toUserAssetEntry(
a,
context.tradingMode,
escapableMap,
context.freezeStatus,
assetPrices,
postionValues,
collateralAsset?.assetId,
assetDetailsMap
)
)

const hideAllAssets =
context.freezeStatus === 'frozen' &&
context.tradingMode === 'perpetual' &&
escapableAssetHashes.length > 0

const content = renderUserAssetsPage({
context,
starkKey,
Expand Down Expand Up @@ -573,6 +643,7 @@ function toUserAssetEntry(
escapableMap: EscapableMap,
freezeStatus: FreezeStatus,
assetPrices: PricesRecord[],
positionValues: PositionValue | undefined,
collateralAssetId?: AssetId,
assetDetailsMap?: AssetDetailsMap
): UserAssetEntry {
Expand All @@ -597,6 +668,8 @@ function toUserAssetEntry(
// We need to use the latest price for the asset from PricesRepository.
const assetPrice = assetPrices.find((p) => p.assetId === asset.assetHashOrId)

const positionValue =
positionValues?.fundingPayments[asset.assetHashOrId.toString()]
return {
asset: {
hashOrId: asset.assetHashOrId,
Expand All @@ -611,7 +684,8 @@ function toUserAssetEntry(
: assetPrice !== undefined
? getAssetValueUSDCents(asset.balance, assetPrice.price)
: undefined,

fundingPayment:
positionValue !== undefined ? positionValue / 10000n : undefined,
vaultOrPositionId: asset.positionOrVaultId.toString(),
action,
}
Expand Down
54 changes: 54 additions & 0 deletions packages/backend/src/utils/calculatePositionValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { State } from '@explorer/encoding'
import { MerkleProof, PositionLeaf } from '@explorer/state'

const FXP_BITS = 32n

export interface PositionValue {
fundingPayments: Record<string, bigint>
positionValue: bigint | undefined
}

export function calculatePositionValue(
merkleProof: MerkleProof<PositionLeaf>,
state: State
): PositionValue {
const position = merkleProof.leaf
const fundingPayments: Record<string, bigint> = {}
let fxpBalance = position.collateralBalance << FXP_BITS

// For each asset in the position
for (const asset of position.assets) {
// Find the current funding index for this asset
const fundingIndex = state.indices.find(
(idx) => idx.assetId === asset.assetId
)
if (!fundingIndex) {
throw new Error(
`Funding index not found for asset ${asset.assetId.toString()}`
)
}

// Calculate funding payment
const fundingPayment =
asset.balance * (asset.fundingIndex - fundingIndex.value)

// Find the current price for this asset
const priceData = state.oraclePrices.find(
(price) => price.assetId === asset.assetId
)
if (!priceData) {
throw new Error(`Price not found for asset ${asset.assetId.toString()}`)
}

// Update the balance based on asset value and funding
fxpBalance += asset.balance * priceData.price + fundingPayment

// Store funding payment for this asset
fundingPayments[asset.assetId.toString()] = fundingPayment >> FXP_BITS
}

return {
fundingPayments,
positionValue: fxpBalance >> FXP_BITS,
}
}
1 change: 1 addition & 0 deletions packages/frontend/src/preview/data/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function randomUserAssetEntry(
asset: asset ?? assetBucket.pick(),
balance: amountBucket.pick(),
value: amountBucket.pick(),
fundingPayment: amountBucket.pick(),
action: action ?? actionBucket.pick(),
vaultOrPositionId: randomId(),
}
Expand Down
Loading

0 comments on commit 5275df7

Please sign in to comment.