Skip to content

Commit

Permalink
feat(minifront): #1149: sort assets table by priority score (#1403)
Browse files Browse the repository at this point in the history
* feat(minifront): #1149: add priority sorting to the assets table

* chore: changeset

* fix(minifront): #1149: update after the merge

* feat(services, minifront): #1149: assign priorityScores in services package

* fix(minifront): #1149: update priority score sorting

* chore: changesets

* fix(services): #1149: update scoring assignment
  • Loading branch information
VanishMax authored Jul 4, 2024
1 parent 2520526 commit 066aabb
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/smart-apples-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/services': minor
---

Add priority scores to the asset metadata
5 changes: 5 additions & 0 deletions .changeset/tough-lemons-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'minifront': patch
---

Add priority sorting to the assets table
16 changes: 12 additions & 4 deletions apps/minifront/src/components/dashboard/assets-table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
import { ValueViewComponent } from '@repo/ui/components/ui/value';
import { EquivalentValues } from './equivalent-values';
import { Fragment } from 'react';
import { shouldDisplay } from './helpers';
import { PagePath } from '../../metadata/paths';
import { Link } from 'react-router-dom';
import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response';
import { getAddressIndex } from '@penumbra-zone/getters/address-view';
import { balancesByAccountSelector, useBalancesResponses } from '../../../state/shared';
import { BalancesByAccount, groupByAccount, useBalancesResponses } from '../../../state/shared';
import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types';
import { shouldDisplay } from '../../../fetchers/balances/should-display';
import { sortByPriorityScore } from '../../../fetchers/balances/by-priority-score';

const getTradeLink = (balance: BalancesResponse): string => {
const metadata = getMetadataFromBalancesResponseOptional(balance);
Expand All @@ -26,9 +28,15 @@ const getTradeLink = (balance: BalancesResponse): string => {
return metadata ? `${PagePath.SWAP}?from=${metadata.symbol}${accountQuery}` : PagePath.SWAP;
};

const filteredBalancesByAccountSelector = (
zQueryState: AbridgedZQueryState<BalancesResponse[]>,
): BalancesByAccount[] =>
zQueryState.data?.filter(shouldDisplay).sort(sortByPriorityScore).reduce(groupByAccount, []) ??
[];

export default function AssetsTable() {
const balancesByAccount = useBalancesResponses({
select: balancesByAccountSelector,
select: filteredBalancesByAccountSelector,
shouldReselect: (before, after) => before?.data !== after.data,
});

Expand Down Expand Up @@ -75,7 +83,7 @@ export default function AssetsTable() {
</TableRow>
</TableHeader>
<TableBody>
{account.balances.filter(shouldDisplay).map((assetBalance, index) => (
{account.balances.map((assetBalance, index) => (
<TableRow className='group' key={index}>
<TableCell>
<ValueViewComponent view={assetBalance.balanceView} />
Expand Down
32 changes: 0 additions & 32 deletions apps/minifront/src/fetchers/balances/by-account.ts

This file was deleted.

23 changes: 23 additions & 0 deletions apps/minifront/src/fetchers/balances/by-priority-score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import {
getMetadataFromBalancesResponseOptional,
getAmount,
} from '@penumbra-zone/getters/balances-response';
import { multiplyAmountByNumber, joinLoHiAmount } from '@penumbra-zone/types/amount';

export const sortByPriorityScore = (a: BalancesResponse, b: BalancesResponse) => {
const aScore = getMetadataFromBalancesResponseOptional(a)?.priorityScore ?? 1n;
const bScore = getMetadataFromBalancesResponseOptional(b)?.priorityScore ?? 1n;

const aAmount = getAmount.optional()(a);
const bAmount = getAmount.optional()(b);

const aPriority = aAmount
? joinLoHiAmount(multiplyAmountByNumber(aAmount, Number(aScore)))
: aScore;
const bPriority = bAmount
? joinLoHiAmount(multiplyAmountByNumber(bAmount, Number(bScore)))
: bScore;

return Number(bPriority - aPriority);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assetPatterns } from '@penumbra-zone/types/assets';
import { getDisplay } from '@penumbra-zone/getters/metadata';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { getMetadata } from '@penumbra-zone/getters/value-view';
import { isKnown } from '../../../state/helpers';
import { isKnown } from '../../state/helpers';

// We don't have to disclose auctionNft to the user since it is a kind of utility asset needed only
// for the implementation of the Dutch auction
Expand Down
3 changes: 1 addition & 2 deletions apps/minifront/src/fetchers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const getChains = async (): Promise<Chain[]> => {
const chainId = await getChainId();
if (!chainId) throw new Error('Could not fetch chain id');

const registryClient = new ChainRegistryClient();
const { ibcConnections } = registryClient.get(chainId);
const { ibcConnections } = chainRegistryClient.get(chainId);
return ibcConnections;
};
5 changes: 4 additions & 1 deletion apps/minifront/src/state/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ export interface BalancesByAccount {
balances: BalancesResponse[];
}

const groupByAccount = (acc: BalancesByAccount[], curr: BalancesResponse): BalancesByAccount[] => {
export const groupByAccount = (
acc: BalancesByAccount[],
curr: BalancesResponse,
): BalancesByAccount[] => {
const index = getAddressIndex(curr.accountAddress);
const grouping = acc.find(a => a.account === index.account);

Expand Down
17 changes: 16 additions & 1 deletion packages/services/src/view-service/asset-metadata-by-id.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Impl } from '.';
import { servicesCtx } from '../ctx/prax';
import { assetPatterns } from '@penumbra-zone/types/assets';
import { getAssetPriorityScore } from './util/asset-priority-score';

export const assetMetadataById: Impl['assetMetadataById'] = async ({ assetId }, ctx) => {
if (!assetId) throw new Error('No asset id passed in request');
Expand All @@ -14,9 +15,23 @@ export const assetMetadataById: Impl['assetMetadataById'] = async ({ assetId },
const { indexedDb, querier } = await services.getWalletServices();

const localMetadata = await indexedDb.getAssetsMetadata(assetId);
if (localMetadata) return { denomMetadata: localMetadata };
if (localMetadata) {
if (!localMetadata.priorityScore) {
localMetadata.priorityScore = getAssetPriorityScore(
localMetadata,
indexedDb.stakingTokenAssetId,
);
}
return { denomMetadata: localMetadata };
}

const remoteMetadata = await querier.shieldedPool.assetMetadataById(assetId);
if (remoteMetadata && !remoteMetadata.priorityScore) {
remoteMetadata.priorityScore = getAssetPriorityScore(
remoteMetadata,
indexedDb.stakingTokenAssetId,
);
}

const isIbcAsset = remoteMetadata && assetPatterns.ibc.matches(remoteMetadata.display);

Expand Down
2 changes: 1 addition & 1 deletion packages/services/src/view-service/balances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('Balances request handler', () => {
expect(getEquivalentValues(response.balanceView)).toEqual([
new EquivalentValue({
asOfHeight: 123n,
numeraire: { penumbraAssetId: numeraire },
numeraire: { penumbraAssetId: numeraire, priorityScore: 40n },
equivalentAmount,
}),
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { getAssetPriorityScore } from './asset-priority-score';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { base64ToUint8Array } from '@penumbra-zone/types/base64';

describe('getAssetPriorityScore', () => {
const umTokenId = 'KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA=';
const umToken = new AssetId({ inner: base64ToUint8Array(umTokenId) });
const delegationTokenId = '/5AHh95RAybBbUhQ5zXMWCvstH4rRK/5KMVIVGQltAw=';
const usdcTokenId = 'A/8PdbaWqFds9NiYzmAN75SehGpkLwr7tgoVmwaIVgg=';

it('returns 0 for the undefined metadata', () => {
expect(getAssetPriorityScore(undefined, umToken)).toBe(0n);
});

it('returns 10 for an unbonding token', () => {
const metadata = new Metadata({
display: 'unbonding_start_at_100_penumbravalid1abc123',
});

expect(getAssetPriorityScore(metadata, umToken)).toBe(10n);
});

it('returns 20 for a delegation token', () => {
const metadata = new Metadata({
display:
'delegation_penumbravalid1sqwq8p8fqxx4aflthtwmu6kte8je7sh4tj7pyd82qpvdap5ajgrsv0q0ja',
penumbraAssetId: {
inner: base64ToUint8Array(delegationTokenId),
},
});

expect(getAssetPriorityScore(metadata, umToken)).toBe(20n);
});

it('returns 30 for an auction token', () => {
const metadata = new Metadata({
display: 'auctionnft_0_pauctid1abc123',
});

expect(getAssetPriorityScore(metadata, umToken)).toBe(30n);
});

it('returns 40 for an token within registry', () => {
const metadata = new Metadata({
display: 'transfer/channel-7/usdc',
penumbraAssetId: {
inner: base64ToUint8Array(usdcTokenId),
},
});

expect(getAssetPriorityScore(metadata, umToken)).toBe(40n);
});

it('returns 50 for the UM token', () => {
const metadata = new Metadata({
penumbraAssetId: {
inner: base64ToUint8Array(umTokenId),
},
});

expect(getAssetPriorityScore(metadata, umToken)).toBe(50n);
});
});
50 changes: 50 additions & 0 deletions packages/services/src/view-service/util/asset-priority-score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { assetPatterns } from '@penumbra-zone/types/assets';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

/**
* Assigns a priority score to an asset based on its metadata.
* The higher the score, the earlier the asset will be displayed in the UI.
* - UM → 50 da
* - Normal and IBC denoms → 40
* - Auctions → 30
* - Delegations → 20
* - Unbondings, proposals, voting receipts → 10
* - Unknown → 0
*
* If a user has the balance of the asset, then the balance amount should be multiplied by this score in a sorting function.
*
* @param metadata {Metadata} – Asset metadata to assign the priority score
* @param nativeTokenId {AssetId} – AssetId of the native chain token (UM)
*/
export const getAssetPriorityScore = (
metadata: Metadata | undefined,
nativeTokenId: AssetId,
): bigint => {
if (!metadata) return 0n;

if (metadata.penumbraAssetId?.equals(nativeTokenId)) {
return 50n;
}

if (assetPatterns.ibc.matches(metadata.display)) return 40n;

if (
assetPatterns.auctionNft.matches(metadata.display) ||
assetPatterns.lpNft.matches(metadata.display)
)
return 30n;

if (assetPatterns.delegationToken.matches(metadata.display)) return 20n;

if (
assetPatterns.unbondingToken.matches(metadata.display) ||
assetPatterns.proposalNft.matches(metadata.display) ||
assetPatterns.votingReceipt.matches(metadata.display)
)
return 10n;

return 40n;
};

0 comments on commit 066aabb

Please sign in to comment.