Skip to content

Commit

Permalink
Show current price in user assets (#526)
Browse files Browse the repository at this point in the history
* show current price in user assets

* fix condition

---------

Co-authored-by: Adrian Adamiak <[email protected]>
  • Loading branch information
torztomasz and adamiak authored Oct 15, 2024
1 parent 32dee7b commit 4e67d8c
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 15 deletions.
3 changes: 3 additions & 0 deletions packages/backend/src/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import { PreprocessedStateDetailsRepository } from './peripherals/database/Prepr
import { PreprocessedStateUpdateRepository } from './peripherals/database/PreprocessedStateUpdateRepository'
import { PreprocessedUserL2TransactionsStatisticsRepository } from './peripherals/database/PreprocessedUserL2TransactionsStatisticsRepository'
import { PreprocessedUserStatisticsRepository } from './peripherals/database/PreprocessedUserStatisticsRepository'
import { PricesRepository } from './peripherals/database/PricesRepository'
import { Database } from './peripherals/database/shared/Database'
import { StateTransitionRepository } from './peripherals/database/StateTransitionRepository'
import { StateUpdateRepository } from './peripherals/database/StateUpdateRepository'
Expand Down Expand Up @@ -129,6 +130,7 @@ export class Application {

const kvStore = new KeyValueStore(database, logger)

const pricesRepository = new PricesRepository(database, logger)
const verifierEventRepository = new VerifierEventRepository(
database,
logger
Expand Down Expand Up @@ -607,6 +609,7 @@ export class Application {
pageContextService,
assetDetailsService,
preprocessedAssetHistoryRepository,
pricesRepository,
sentTransactionRepository,
userTransactionRepository,
forcedTradeOfferRepository,
Expand Down
45 changes: 32 additions & 13 deletions packages/backend/src/api/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
import { sumUpTransactionCount } from '../../peripherals/database/PreprocessedL2TransactionsStatistics'
import { PreprocessedUserL2TransactionsStatisticsRepository } from '../../peripherals/database/PreprocessedUserL2TransactionsStatisticsRepository'
import { PreprocessedUserStatisticsRepository } from '../../peripherals/database/PreprocessedUserStatisticsRepository'
import {
PricesRecord,
PricesRepository,
} from '../../peripherals/database/PricesRepository'
import {
SentTransactionRecord,
SentTransactionRepository,
Expand All @@ -61,6 +65,7 @@ export class UserController {
private readonly pageContextService: PageContextService,
private readonly assetDetailsService: AssetDetailsService,
private readonly preprocessedAssetHistoryRepository: PreprocessedAssetHistoryRepository,
private readonly pricesRepository: PricesRepository,
private readonly sentTransactionRepository: SentTransactionRepository,
private readonly userTransactionRepository: UserTransactionRepository,
private readonly forcedTradeOfferRepository: ForcedTradeOfferRepository,
Expand Down Expand Up @@ -150,6 +155,7 @@ export class UserController {
const [
registeredUser,
userAssets,
assetPrices,
history,
l2Transactions,
preprocessedUserL2TransactionsStatistics,
Expand All @@ -169,6 +175,7 @@ export class UserController {
paginationOpts,
collateralAsset?.assetId
),
this.pricesRepository.getAllLatest(),
this.preprocessedAssetHistoryRepository.getByStarkKeyPaginated(
starkKey,
paginationOpts
Expand Down Expand Up @@ -239,6 +246,7 @@ export class UserController {
context.tradingMode,
escapableMap,
context.freezeStatus,
assetPrices,
collateralAsset?.assetId,
assetDetailsMap
)
Expand Down Expand Up @@ -322,15 +330,19 @@ export class UserController {
): Promise<ControllerResult> {
const context = await this.pageContextService.getPageContext(givenUser)
const collateralAsset = this.pageContextService.getCollateralAsset(context)
const [registeredUser, userAssets, userStatistics] = await Promise.all([
this.userRegistrationEventRepository.findByStarkKey(starkKey),
this.preprocessedAssetHistoryRepository.getCurrentByStarkKeyPaginated(
starkKey,
pagination,
collateralAsset?.assetId
),
this.preprocessedUserStatisticsRepository.findCurrentByStarkKey(starkKey),
])
const [registeredUser, userAssets, assetPrices, userStatistics] =
await Promise.all([
this.userRegistrationEventRepository.findByStarkKey(starkKey),
this.preprocessedAssetHistoryRepository.getCurrentByStarkKeyPaginated(
starkKey,
pagination,
collateralAsset?.assetId
),
this.pricesRepository.getAllLatest(),
this.preprocessedUserStatisticsRepository.findCurrentByStarkKey(
starkKey
),
])

if (!userStatistics) {
return {
Expand Down Expand Up @@ -363,6 +375,7 @@ export class UserController {
context.tradingMode,
escapableMap,
context.freezeStatus,
assetPrices,
collateralAsset?.assetId,
assetDetailsMap
)
Expand Down Expand Up @@ -551,6 +564,7 @@ function toUserAssetEntry(
tradingMode: TradingMode,
escapableMap: EscapableMap,
freezeStatus: FreezeStatus,
assetPrices: PricesRecord[],
collateralAssetId?: AssetId,
assetDetailsMap?: AssetDetailsMap
): UserAssetEntry {
Expand All @@ -571,6 +585,10 @@ function toUserAssetEntry(
}
}

// Price from preprocessedAssetHistory is a price from the moment when the position was opened.
// We need to use the latest price for the asset from PricesRepository.
const assetPrice = assetPrices.find((p) => p.assetId === asset.assetHashOrId)

return {
asset: {
hashOrId: asset.assetHashOrId,
Expand All @@ -580,11 +598,12 @@ function toUserAssetEntry(
},
balance: asset.balance,
value:
asset.price === undefined
? 0n
: asset.assetHashOrId === collateralAssetId
asset.assetHashOrId === collateralAssetId
? asset.balance / 10000n // TODO: use the correct decimals
: getAssetValueUSDCents(asset.balance, asset.price),
: assetPrice !== undefined
? getAssetValueUSDCents(asset.balance, assetPrice.price)
: undefined,

vaultOrPositionId: asset.positionOrVaultId.toString(),
action,
}
Expand Down
76 changes: 76 additions & 0 deletions packages/backend/src/peripherals/database/PricesRepository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { AssetId, Hash256, PedersenHash, Timestamp } from '@explorer/types'
import { Logger } from '@l2beat/backend-tools'
import { expect } from 'earl'
import { it } from 'mocha'

import { setupDatabaseTestSuite } from '../../test/database'
import { PricesRepository } from './PricesRepository'
import { StateUpdateRepository } from './StateUpdateRepository'

describe(PricesRepository.name, () => {
const { database } = setupDatabaseTestSuite()
const stateUpdateRepository = new StateUpdateRepository(
database,
Logger.SILENT
)
const repository = new PricesRepository(database, Logger.SILENT)

afterEach(() => repository.deleteAll())

describe(PricesRepository.prototype.getAllLatest.name, () => {
it('returns prices for all assets for the latest state update', async () => {
await stateUpdateRepository.add(mockStateUpdate(1))
await stateUpdateRepository.add(mockStateUpdate(199))
await stateUpdateRepository.add(mockStateUpdate(200))

await repository.add({
stateUpdateId: 200,
assetId: AssetId('MATIC-6'),
price: 100n,
})
await repository.add({
stateUpdateId: 200,
assetId: AssetId('LTC-8'),
price: 100n,
})
await repository.add({
stateUpdateId: 200,
assetId: AssetId('ETH-9'),
price: 100n,
})
await repository.add({
stateUpdateId: 199,
assetId: AssetId('USDC-6'),
price: 100n,
})
await repository.add({
stateUpdateId: 1,
assetId: AssetId('ETH-9'),
price: 100n,
})

const results = await repository.getAllLatest()
expect(results).toEqualUnsorted([
{ stateUpdateId: 200, assetId: AssetId('MATIC-6'), price: 100n },
{ stateUpdateId: 200, assetId: AssetId('LTC-8'), price: 100n },
{ stateUpdateId: 200, assetId: AssetId('ETH-9'), price: 100n },
])
})
})
})

function mockStateUpdate(id: number) {
return {
stateUpdate: {
id,
batchId: id - 1,
blockNumber: id,
rootHash: PedersenHash.fake(),
stateTransitionHash: Hash256.fake(),
timestamp: Timestamp(0),
},
positions: [],
prices: [],
transactionHashes: [],
}
}
75 changes: 75 additions & 0 deletions packages/backend/src/peripherals/database/PricesRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { AssetId } from '@explorer/types'
import { Logger } from '@l2beat/backend-tools'
import { PriceRow } from 'knex/types/tables'

import { BaseRepository } from './shared/BaseRepository'
import { Database } from './shared/Database'

export interface PricesRecord {
stateUpdateId: number
assetId: AssetId
price: bigint
}

export class PricesRepository extends BaseRepository {
constructor(database: Database, logger: Logger) {
super(database, logger)

/* eslint-disable @typescript-eslint/unbound-method */
this.add = this.wrapAdd(this.add)
this.getAllLatest = this.wrapGet(this.getAllLatest)
this.deleteAll = this.wrapDelete(this.deleteAll)
/* eslint-enable @typescript-eslint/unbound-method */
}

async add(record: PricesRecord): Promise<number> {
const knex = await this.knex()
const results = await knex('prices')
.insert(toPriceRow(record))
.returning('state_update_id')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return results[0]!.state_update_id
}

async getAllLatest() {
const knex = await this.knex()

const rows = await knex('prices')
.select('*')
.innerJoin(
knex('prices')
.max('state_update_id as max_state_update_id')
.as('latest_prices'),
(join) => {
join.on(
'prices.state_update_id',
'=',
'latest_prices.max_state_update_id'
)
}
)

return rows.map(toPricesRecord)
}

async deleteAll() {
const knex = await this.knex()
return await knex('prices').delete()
}
}

function toPricesRecord(row: PriceRow): PricesRecord {
return {
stateUpdateId: row.state_update_id,
assetId: AssetId(row.asset_id),
price: BigInt(row.price),
}
}

function toPriceRow(record: PricesRecord): PriceRow {
return {
state_update_id: record.stateUpdateId,
asset_id: record.assetId.toString(),
price: record.price,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface UserAssetsTableProps {
export interface UserAssetEntry {
asset: Asset
balance: bigint
value: bigint
value: bigint | undefined
vaultOrPositionId: string
action:
| 'WITHDRAW'
Expand Down Expand Up @@ -62,7 +62,9 @@ export function UserAssetsTable(props: UserAssetsTableProps) {
</span>
{props.tradingMode === 'perpetual' && (
<span className="mt-2 text-xxs text-zinc-500">
{formatWithDecimals(entry.value, 2, { prefix: '$' })}
{entry.value
? formatWithDecimals(entry.value, 2, { prefix: '$' })
: 'Unknown price'}
</span>
)}
</div>,
Expand Down

0 comments on commit 4e67d8c

Please sign in to comment.