Skip to content

Commit b95dc9b

Browse files
authored
Asset page (#722)
1 parent 69ae8ae commit b95dc9b

36 files changed

+2489
-16
lines changed

src/modules/remote-config/plugins/firebase.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ const defaultConfig: RemoteConfig = {
99
extension_wallet_name_flags: {},
1010
extension_uninstall_link: '',
1111
extension_loyalty_enabled: true,
12+
extension_asset_page_enabled: false,
1213
loyalty_config: {},
1314
};
1415

1516
const knownKeys: (keyof RemoteConfig)[] = [
1617
'extension_wallet_name_flags',
1718
'extension_uninstall_link',
1819
'extension_loyalty_enabled',
20+
'extension_asset_page_enabled',
1921
'loyalty_config',
2022
];
2123

src/modules/remote-config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface RemoteConfig {
44
extension_wallet_name_flags: Record<string, WalletNameFlag[]>;
55
extension_uninstall_link: string;
66
extension_loyalty_enabled: boolean;
7+
extension_asset_page_enabled: boolean;
78
loyalty_config: Partial<{
89
referrerXpPercent: number;
910
gasbackValue: number;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { ZerionAPI } from 'src/modules/zerion-api/zerion-api.client';
3+
import { type Params } from '../requests/asset-get-fungible-full-info';
4+
import type { BackendSourceParams } from '../shared';
5+
6+
export function useAssetFullInfo(
7+
params: Params,
8+
{ source }: BackendSourceParams,
9+
{ suspense = false }: { suspense?: boolean } = {}
10+
) {
11+
return useQuery({
12+
queryKey: ['assetGetFungibleFullInfo', params, source],
13+
queryFn: () => ZerionAPI.assetGetFungibleFullInfo(params, { source }),
14+
suspense,
15+
staleTime: 20000,
16+
});
17+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import type { Params } from '../requests/wallet-get-asset-details';
3+
import { ZerionAPI } from '../zerion-api.client';
4+
import type { BackendSourceParams } from '../shared';
5+
6+
export function useWalletAssetDetails(
7+
params: Params,
8+
{ source }: BackendSourceParams,
9+
{
10+
suspense = false,
11+
enabled = true,
12+
}: {
13+
suspense?: boolean;
14+
enabled?: boolean;
15+
} = {}
16+
) {
17+
return useQuery({
18+
queryKey: ['walletGetAssetDetails', params, source],
19+
queryFn: () => ZerionAPI.walletGetAssetDetails(params, { source }),
20+
suspense,
21+
enabled,
22+
staleTime: 20000,
23+
});
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import type { Params } from '../requests/asset-get-fungible-pnl';
3+
import { ZerionAPI } from '../zerion-api.client';
4+
import type { BackendSourceParams } from '../shared';
5+
6+
export function useWalletAssetPnl(
7+
params: Params,
8+
{ source }: BackendSourceParams,
9+
{
10+
suspense = false,
11+
enabled = true,
12+
}: {
13+
suspense?: boolean;
14+
enabled?: boolean;
15+
} = {}
16+
) {
17+
return useQuery({
18+
queryKey: ['assetGetFungiblePnl', params, source],
19+
queryFn: () => ZerionAPI.assetGetFungiblePnl(params, { source }),
20+
suspense,
21+
enabled,
22+
staleTime: 20000,
23+
});
24+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { ClientOptions } from '../shared';
2+
import { CLIENT_DEFAULTS, ZerionHttpClient } from '../shared';
3+
import type { ZerionApiContext } from '../zerion-api-bare';
4+
import type { ResponseBody } from './ResponseBody';
5+
6+
export interface Params {
7+
fungibleId: string;
8+
currency: string;
9+
}
10+
11+
export interface Asset {
12+
id: string;
13+
iconUrl: string | null;
14+
name: string;
15+
new: boolean;
16+
symbol: string;
17+
verified: boolean;
18+
implementations: Record<string, { address: string | null; decimals: number }>;
19+
meta: {
20+
circulatingSupply: number | null;
21+
fullyDilutedValuation: number | null;
22+
marketCap: number | null;
23+
price: number | null;
24+
relativeChange1d: number | null;
25+
relativeChange30d: number | null;
26+
relativeChange90d: number | null;
27+
relativeChange365d: number | null;
28+
totalSupply: number | null;
29+
};
30+
}
31+
32+
export interface AssetResource {
33+
name: string;
34+
url: string;
35+
iconUrl: string;
36+
displayableName: string;
37+
}
38+
39+
export interface AssetFullInfo {
40+
extra: {
41+
createdAt: string;
42+
description: string | null;
43+
holders: null;
44+
liquidity: null;
45+
top10: null;
46+
volume24h: null;
47+
relevantResources: AssetResource[];
48+
mainChain: string;
49+
};
50+
fungible: Asset;
51+
}
52+
53+
type Response = ResponseBody<AssetFullInfo>;
54+
55+
export async function assetGetFungibleFullInfo(
56+
this: ZerionApiContext,
57+
payload: Params,
58+
options: ClientOptions = CLIENT_DEFAULTS
59+
) {
60+
const params = new URLSearchParams({
61+
fungibleId: payload.fungibleId,
62+
currency: payload.currency,
63+
});
64+
return ZerionHttpClient.get<Response>({
65+
endpoint: `asset/get-fungible-full-info/v1?${params}`,
66+
...options,
67+
});
68+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ClientOptions } from '../shared';
2+
import { CLIENT_DEFAULTS, ZerionHttpClient } from '../shared';
3+
import type { ZerionApiContext } from '../zerion-api-bare';
4+
import type { ResponseBody } from './ResponseBody';
5+
6+
export interface Params {
7+
fungibleId: string;
8+
addresses: string[];
9+
currency: string;
10+
}
11+
12+
export interface AssetAddressPnl {
13+
realizedPnl: number;
14+
unrealizedPnl: number;
15+
totalPnl: number;
16+
relativeRealizedPnl: number;
17+
relativeUnrealizedPnl: number;
18+
relativeTotalPnl: number;
19+
averageBuyPrice: number;
20+
bought: number;
21+
}
22+
23+
type Response = ResponseBody<AssetAddressPnl>;
24+
25+
export async function assetGetFungiblePnl(
26+
this: ZerionApiContext,
27+
params: Params,
28+
options: ClientOptions = CLIENT_DEFAULTS
29+
) {
30+
const firstAddress = params.addresses[0];
31+
const provider = await this.getAddressProviderHeader(firstAddress);
32+
return ZerionHttpClient.post<Response>({
33+
endpoint: 'asset/get-fungible-pnl/v1',
34+
body: JSON.stringify(params),
35+
headers: { 'Zerion-Wallet-Provider': provider },
36+
...options,
37+
});
38+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { ClientOptions } from '../shared';
2+
import { CLIENT_DEFAULTS, ZerionHttpClient } from '../shared';
3+
import type { ZerionApiContext } from '../zerion-api-bare';
4+
import type { ResponseBody } from './ResponseBody';
5+
6+
export interface Params {
7+
assetId: string;
8+
addresses: string[];
9+
currency: string;
10+
groupBy: ('by-wallet' | 'by-app')[];
11+
}
12+
13+
interface NetworkShortInfo {
14+
id: string;
15+
name: string;
16+
iconUrl: string;
17+
testnet: boolean;
18+
}
19+
20+
export interface WalletAssetDetails {
21+
chainsDistribution: Array<{
22+
chain: NetworkShortInfo;
23+
value: number;
24+
percentageAllocation: number;
25+
}> | null;
26+
wallets: Array<{
27+
wallet: {
28+
name: string;
29+
iconUrl: string;
30+
premium: boolean;
31+
address: string;
32+
};
33+
chains: NetworkShortInfo[];
34+
value: number;
35+
convertedQuantity: number;
36+
percentageAllocation: number;
37+
}>;
38+
apps: Array<{
39+
app: {
40+
id: string;
41+
name: string;
42+
iconUrl: string | null;
43+
url: string | null;
44+
};
45+
chains: NetworkShortInfo[];
46+
value: number;
47+
convertedQuantity: number;
48+
percentageAllocation: number;
49+
}> | null;
50+
totalValue: number;
51+
totalConvertedQuantity: number;
52+
}
53+
54+
type Response = ResponseBody<WalletAssetDetails>;
55+
56+
export async function walletGetAssetDetails(
57+
this: ZerionApiContext,
58+
params: Params,
59+
options: ClientOptions = CLIENT_DEFAULTS
60+
) {
61+
const firstAddress = params.addresses[0];
62+
const provider = await this.getAddressProviderHeader(firstAddress);
63+
return ZerionHttpClient.post<Response>({
64+
endpoint: 'wallet/get-asset-details/v1',
65+
body: JSON.stringify(params),
66+
headers: { 'Zerion-Wallet-Provider': provider },
67+
...options,
68+
});
69+
}

src/modules/zerion-api/zerion-api-bare.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import { walletGetPortfolio } from './requests/wallet-get-portfolio';
1515
import { checkReferral } from './requests/check-referral';
1616
import { referWallet } from './requests/refer-wallet';
1717
import { claimRetro } from './requests/claim-retro';
18+
import { assetGetFungibleFullInfo } from './requests/asset-get-fungible-full-info';
19+
import { walletGetAssetDetails } from './requests/wallet-get-asset-details';
20+
import { assetGetFungiblePnl } from './requests/asset-get-fungible-pnl';
1821

1922
export interface ZerionApiContext {
2023
getAddressProviderHeader(address: string): Promise<string>;
@@ -34,6 +37,9 @@ export const ZerionApiBare = {
3437
checkReferral,
3538
referWallet,
3639
claimRetro,
40+
assetGetFungibleFullInfo,
41+
assetGetFungiblePnl,
42+
walletGetAssetDetails,
3743
};
3844

3945
export type ZerionApiClient = typeof ZerionApiBare;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import BigNumber from 'bignumber.js';
2+
import memoize from 'memoize-one';
3+
import type { CurrencyConfig } from 'src/modules/currency/currencies';
4+
import { CURRENCIES, resolveOptions } from 'src/modules/currency/currencies';
5+
import { minus as typographicMinus } from 'src/ui/shared/typography';
6+
7+
const getCurrencyFormatter = memoize((locale, currency, config = {}) => {
8+
return new Intl.NumberFormat(locale, {
9+
style: 'currency',
10+
currency,
11+
...config,
12+
});
13+
});
14+
15+
const getSmallPriceCurrencyFormatter = memoize(
16+
(locale, currency, config = {}) => {
17+
return new Intl.NumberFormat(locale, {
18+
style: 'currency',
19+
currency,
20+
minimumFractionDigits: 2,
21+
maximumSignificantDigits: 3,
22+
maximumFractionDigits: 20,
23+
...config,
24+
});
25+
}
26+
);
27+
28+
// ~1$ values should be formatted as regular currency values
29+
const SMALL_VALUE_THRESHOLD = 0.99;
30+
31+
export function formatPriceValue(
32+
value: BigNumber.Value,
33+
locale: string,
34+
currency: string,
35+
opts: Intl.NumberFormatOptions | null = null
36+
) {
37+
const number = value instanceof BigNumber ? value.toNumber() : Number(value);
38+
const sign = number < 0 ? typographicMinus : '';
39+
const absValue = Math.abs(number);
40+
const isSmallValue = absValue < SMALL_VALUE_THRESHOLD;
41+
42+
const config = CURRENCIES[currency] as CurrencyConfig | undefined;
43+
const numberFormatOptions = resolveOptions(number, config || null, opts);
44+
const formatter = isSmallValue
45+
? getSmallPriceCurrencyFormatter(locale, currency, numberFormatOptions)
46+
: getCurrencyFormatter(locale, currency, numberFormatOptions);
47+
48+
const modifyParts = config?.modifyParts;
49+
if (modifyParts) {
50+
const parts = formatter.formatToParts(absValue);
51+
return `${sign}${modifyParts(parts)
52+
.map((part) => part.value)
53+
.join('')}`;
54+
}
55+
return `${sign}${formatter.format(absValue)}`;
56+
}

0 commit comments

Comments
 (0)