Skip to content

Commit

Permalink
Merge pull request #3392 from Emurgo/ruslan/YOEXT-801/dashboard-pool-…
Browse files Browse the repository at this point in the history
…info

dashboard pool info
  • Loading branch information
vsubhuman authored Jan 23, 2024
2 parents 8ef621e + 3f48a49 commit 89dcea1
Show file tree
Hide file tree
Showing 18 changed files with 179 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { CardanoAssetMintMetadata, NetworkRow, } from '../../../api/ada/lib
import { NftImage } from './NFTsList';
import { isCardanoHaskell } from '../../../api/ada/lib/storage/database/prepackaged/networks';
import { truncateAddress, truncateAddressShort } from '../../../utils/formatters';
import { urlResolveIpfs } from '../../../coreUtils';
import { urlResolveForIpfsAndCorsproxy } from '../../../coreUtils';
import { ampli } from '../../../../ampli/index';
import { CopyAddress, TruncatedText } from './TruncatedText';

Expand Down Expand Up @@ -98,7 +98,7 @@ const tabs = [
];

function NFTDetails({ nftInfo, network, intl, nextNftId, prevNftId, tab }: Props & Intl): Node {
const nftImage = urlResolveIpfs(nftInfo?.image);
const nftImage = urlResolveForIpfsAndCorsproxy(nftInfo?.image);
const networkUrl = getNetworkUrl(network);
const [activeTab, setActiveTab] = useState(tab !== null ? tab : tabs[0].id); // Overview tab
const setActiveTabAndTrack = function (tabId: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Link } from 'react-router-dom';
import { ROUTES } from '../../../routes-config';
import { useState, useEffect, useCallback } from 'react';
import globalMessages from '../../../i18n/global-messages';
import { urlResolveIpfs } from '../../../coreUtils';
import { urlResolveForIpfsAndCorsproxy } from '../../../coreUtils';
import classNames from 'classnames';
import { debounce } from 'lodash';
import { ampli } from '../../../../ampli/index';
Expand Down Expand Up @@ -219,7 +219,7 @@ export function NftImage({
|}): Node {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const url = urlResolveIpfs(imageUrl);
const url = urlResolveForIpfsAndCorsproxy(imageUrl);

useEffect(() => {
if (url !== null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Component } from 'react';
import { ReactComponent as DefaultNFT } from '../../../../assets/images/nft-no.inline.svg';
import { checkNFTImage } from '../../../../utils/wallet';
import type { Node } from 'react';
import { urlResolveIpfs } from '../../../../coreUtils';
import { urlResolveForIpfsAndCorsproxy } from '../../../../coreUtils';

type Props = {|
name: string,
Expand All @@ -26,7 +26,7 @@ export default class NFTImage extends Component<Props, State> {
componentDidMount() {
const { image } = this.props;
if (image === null) return;
const imageUrl = urlResolveIpfs(image);
const imageUrl = urlResolveForIpfsAndCorsproxy(image);
checkNFTImage(
imageUrl,
() => {
Expand All @@ -42,7 +42,7 @@ export default class NFTImage extends Component<Props, State> {
const { image, name, width, height } = this.props;
const { loading, error } = this.state;
if (image === null || error) return <DefaultNFT />;
const imageUrl = urlResolveIpfs(image);
const imageUrl = urlResolveForIpfsAndCorsproxy(image);

return loading ? (
<Skeleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function DelegatedStakePoolCard({ delegatedPool, undelegate, intl }: Props & Int
color="grayscale.500"
sx={{ textTransform: 'uppercase' }}
>
{intl.formatMessage(globalMessages.poolShare)}
{intl.formatMessage(globalMessages.poolSaturation)}
</Typography>
<Typography as="span" fontWeight={500} color="grayscale.max" variant="h2">
{share} %
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export type PoolData = {|
+id: string,
+name: string,
+ticker?: string,
+avatar?: string,
+roa?: string,
+poolSize?: string,
+share?: string,
+avatar?: ?string,
+roa?: ?string,
+poolSize?: ?string,
+share?: ?string,
+websiteUrl?: string,
+socialLinks?: SocialLinks,
|};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ import { ApiOptions, getApiForNetwork } from '../../../api/common/utils';
import RewardHistoryDialog from '../../../components/wallet/staking/dashboard-revamp/RewardHistoryDialog';
import DelegatedStakePoolCard from '../../../components/wallet/staking/dashboard-revamp/DelegatedStakePoolCard';
import WithdrawRewardsDialog from './WithdrawRewardsDialog';
import type { PoolInfo } from '@emurgo/yoroi-lib';
import { formatLovelacesHumanReadableShort, roundOneDecimal, roundTwoDecimal } from '../../../utils/formatters';
import { compose, maybe } from '../../../coreUtils';

export type GeneratedData = typeof StakingPageContent.prototype.generated;
// populated by ConfigWebpackPlugin
Expand Down Expand Up @@ -227,39 +230,31 @@ class StakingPageContent extends Component<AllProps> {
}

if (delegationRequests.getDelegatedBalance.result.delegation == null) return null;
const currentPool = delegationRequests.getDelegatedBalance.result.delegation;
const meta = this.generated.stores.delegation.getLocalPoolInfo(
publicDeriver.getParent().getNetworkInfo(),
currentPool
);
if (meta == null) {
const networkInfo = publicDeriver.getParent().getNetworkInfo();
const currentPool: ?string = delegationRequests.getDelegatedBalance.result?.delegation;
const poolMeta = maybe(currentPool,
s => this.generated.stores.delegation.getLocalPoolInfo(networkInfo, s));
const { stake, roa, saturation, pic } = maybe(currentPool,
s => this.generated.stores.delegation.getLocalRemotePoolInfo(networkInfo, s)) ?? {};
if (poolMeta == null) {
// server hasn't returned information about the stake pool yet
return null;
}
const { intl } = this.context;
const name = meta.info?.name ?? intl.formatMessage(globalMessages.unknownPoolLabel);
// TODO: remove placeholders

const name = poolMeta.info?.name ?? intl.formatMessage(globalMessages.unknownPoolLabel);

const delegatedPool = {
id: String(currentPool),
name,
roa: '-',
poolSize: '-',
share: '-',
websiteUrl: meta.info?.homepage,
ticker: meta.info?.ticker,
avatar: pic,
roa: maybe(roa, compose(Number, roundTwoDecimal)),
poolSize: maybe(stake, formatLovelacesHumanReadableShort),
share: maybe(saturation, s => roundOneDecimal(Number(s)*100)),
websiteUrl: poolMeta.info?.homepage,
ticker: poolMeta.info?.ticker,
};

// TODO: implement this eventually
// const stakePoolMeta = {
// avatar: '',
// websiteUrl: '',
// roa: ' 5.08%',
// socialLinks: {
// fb: '',
// tw: '',
// },
// };

return (
<DelegatedStakePoolCard
delegatedPool={delegatedPool}
Expand Down Expand Up @@ -529,6 +524,7 @@ class StakingPageContent extends Component<AllProps> {
delegation: {|
selectedPage: number,
getLocalPoolInfo: ($ReadOnly<NetworkRow>, string) => void | PoolMeta,
getLocalRemotePoolInfo: ($ReadOnly<NetworkRow>, string) => void | PoolInfo,
getDelegationRequests: (PublicDeriver<>) => void | DelegationRequests,
|},
profile: {|
Expand Down Expand Up @@ -589,6 +585,7 @@ class StakingPageContent extends Component<AllProps> {
delegation: {
selectedPage: stores.delegation.selectedPage,
getLocalPoolInfo: stores.delegation.getLocalPoolInfo,
getLocalRemotePoolInfo: stores.delegation.getLocalRemotePoolInfo,
getDelegationRequests: stores.delegation.getDelegationRequests,
},
uiDialogs: {
Expand Down
41 changes: 38 additions & 3 deletions packages/yoroi-extension/app/coreUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export function logErr<T>(f: () => T, msg: (string | (Error) => string)): T {
* In case the URL is at the IPFS protocol it will be resolved into HTTPS.
* In any other case there will be no change in the returned result.
*/
export function urlResolveIpfs<T: ?string>(url: T): T {
// $FlowFixMe
return url?.replace('ipfs://', 'https://ipfs.io/ipfs/');
export function urlResolveForIpfsAndCorsproxy<T: ?string>(url: T): T {
// $FlowIgnore
return maybe(url, (u: string): string => u.startsWith('ipfs://')
? u.replace('ipfs://', 'https://ipfs.io/ipfs/')
: `https://corsproxy.io/${u}`);
}

/**
Expand Down Expand Up @@ -58,3 +60,36 @@ export function createFilterUniqueBy<T>(getter: T => any = x => x): T => boolean
export function listValues<T>(obj: { [any]: T }): T[] {
return ((Object.values(obj): any): T[]);
}

/**
* Aggregates an array of key-value tuples into a map
*/
export function entriesIntoMap<K,V>(col: Array<[K,V]>): { [K]: V } {
return entriesIntoMapBy<[K,V],K,V>(col, x => x);
}

/**
* Converts each object in the array into a key-value tuple, using the provided function,
* and then aggregates tuples into a map
*/
export function entriesIntoMapBy<T,K,V>(col: Array<T>, f: (T => [K,V])): { [K]: V } {
return col.reduce((map, e) => {
const [k, v]: [K, V] = f(e);
map[k] = v;
return map;
},({}: { [K]: V }));
}

/**
* Maps t if != null, otherwise returns same t
*/
export function maybe<T,R>(t: ?T, f: T => ?R): ?R {
return t == null ? t : f(t);
}

/**
* Composes two functions in a null-safe manner
*/
export function compose<A,B,C>(f1: A => ?B, f2: B => ?C): (A => ?C) {
return a => maybe(f1(a), f2);
}
4 changes: 4 additions & 0 deletions packages/yoroi-extension/app/i18n/global-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,10 @@ const globalMessages: * = defineMessages({
id: 'wallet.staking.pool.share',
defaultMessage: '!!!Share',
},
poolSaturation: {
id: 'wallet.staking.pool.saturation',
defaultMessage: '!!!Saturation',
},
sidebarWallets: {
id: 'sidebar.wallets',
defaultMessage: '!!!My wallets',
Expand Down
1 change: 1 addition & 0 deletions packages/yoroi-extension/app/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,7 @@
"wallet.staking.overview": "Overview",
"wallet.staking.overviewContent": "Your rewards are automatically staked. You don’t need to withdraw it everytime because you pay a transaction fee.",
"wallet.staking.pool.share": "Share",
"wallet.staking.pool.saturation": "Saturation",
"wallet.staking.pool.size": "Pool size",
"wallet.staking.pool.unknownLabel": "Unknown pool",
"wallet.staking.rewards.openRewardHistory": "Open Reward History",
Expand Down
36 changes: 18 additions & 18 deletions packages/yoroi-extension/app/stores/ada/AdaDelegationStore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow

import axios from 'axios';
import { action, observable, reaction, runInAction } from 'mobx';
import BigNumber from 'bignumber.js';
import { find } from 'lodash';
Expand Down Expand Up @@ -42,6 +43,10 @@ import { getUnmangleAmounts } from '../stateless/mangledAddresses';
import { MultiToken } from '../../api/common/lib/MultiToken';
import type { ActionsMap } from '../../actions/index';
import type { StoresMap } from '../index';
import { PoolInfoApi } from '@emurgo/yoroi-lib';
import { entriesIntoMap } from '../../coreUtils';
import type { PoolInfo } from '@emurgo/yoroi-lib';
import type { PoolInfoResponse, RemotePool } from '../../api/ada/lib/state-fetch/types';

export type AdaDelegationRequests = {|
publicDeriver: PublicDeriver<>,
Expand Down Expand Up @@ -255,30 +260,24 @@ export default class AdaDelegationStore extends Store<StoresMap, ActionsMap> {
allPoolIds: Array<string>,
|} => Promise<void> = async (request) => {
// update pool information
const poolsCachedForNetwork = this.stores.delegation.poolInfo
.reduce(
(acc, next) => {
if (
next.networkId === request.network.NetworkId
) {
acc.add(next.poolId);
return acc;
}
return acc;
},
(new Set<string>())
);
const poolsToQuery = request.allPoolIds.filter(
pool => !poolsCachedForNetwork.has(pool)
);
const poolsCachedForNetwork = new Set<string>(this.stores.delegation.poolInfo
.filter(next => next.networkId === request.network.NetworkId)
.map(next => next.poolId));
const poolsToQuery = request.allPoolIds.filter(pool => !poolsCachedForNetwork.has(pool));
const stateFetcher = this.stores.substores.ada.stateFetchStore.fetcher;
const poolInfoResp = await stateFetcher.getPoolInfo({
const poolInfoPromise: Promise<PoolInfoResponse> = stateFetcher.getPoolInfo({
network: request.network,
poolIds: poolsToQuery,
});
const remotePoolInfoPromises: Array<Promise<[string, PoolInfo | null]>> =
poolsToQuery.map(id => new PoolInfoApi(axios).getPool(id).then(res => [id, res]));
const [poolInfoResp, remotePoolInfoResps]: [PoolInfoResponse, Array<[string, PoolInfo | null]>] =
await Promise.all([poolInfoPromise, Promise.all(remotePoolInfoPromises)]);
const remoteInfoMap = entriesIntoMap<string, PoolInfo | null>(remotePoolInfoResps);
runInAction(() => {
for (const poolId of Object.keys(poolInfoResp)) {
const poolInfo = poolInfoResp[poolId];
const poolInfo: (RemotePool | null) = poolInfoResp[poolId];
const poolRemoteInfo = remoteInfoMap[poolId];
if (poolInfo == null) continue;
this.stores.delegation.poolInfo.push({
networkId: request.network.NetworkId,
Expand All @@ -293,6 +292,7 @@ export default class AdaDelegationStore extends Store<StoresMap, ActionsMap> {
},
history: poolInfo.history,
},
poolRemoteInfo,
});
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import type { MangledAmountFunc } from '../stateless/mangledAddresses';
import type { ActionsMap } from '../../actions/index';
import type { StoresMap } from '../index';
import type { PoolInfo } from '@emurgo/yoroi-lib';

export type DelegationRequests = {|
publicDeriver: PublicDeriver<>,
Expand Down Expand Up @@ -81,6 +82,7 @@ export default class DelegationStore extends Store<StoresMap, ActionsMap> {
networkId: number,
poolId: string,
poolInfo: PoolMeta,
poolRemoteInfo: PoolInfo | null,
|}> = [];

/**
Expand Down Expand Up @@ -127,6 +129,13 @@ export default class DelegationStore extends Store<StoresMap, ActionsMap> {
return find(this.poolInfo, { networkId: network.NetworkId, poolId })?.poolInfo;
}

getLocalRemotePoolInfo: (
$ReadOnly<NetworkRow>,
string,
) => void | PoolInfo = (network, poolId) => {
return find(this.poolInfo, { networkId: network.NetworkId, poolId })?.poolRemoteInfo ?? undefined;
}

@action.bound
reset(): void {
this.delegationRequests = [];
Expand Down
26 changes: 26 additions & 0 deletions packages/yoroi-extension/app/utils/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,29 @@ export function truncateAddressShort(addr: string, by: ?number): string {
export function formatBigNumberToFloatString(x: BigNumber): string {
return x.isInteger() ? x.toFixed(1) : x.toString();
}

export function formatLovelacesHumanReadableShort(num: string): string {
const fNum = Number(num) / 1000000; // divided in 1,000,000 to convert from Lovelace to ADA
if (fNum >= 1e3) {
const units = ['k', 'M', 'B', 'T'];
// Divide to get SI Unit engineering style numbers (1e3,1e6,1e9, etc)
const unit = Math.floor((fNum.toFixed(0).length - 1) / 3) * 3;
// Calculate the remainder
const formattedNum = ((fNum / Number(`1e${unit}`))).toFixed(2);
const unitname = units[Math.floor(unit / 3) - 1];
return `${formattedNum}${unitname}`;
}
return fNum.toLocaleString();
}

export function roundOneDecimal(num: number): string {
const fNum = Number(num);
const number = Math.round(fNum * 10) / 10;
if(number === 0) return number.toFixed(1)
return number.toString()
}

export function roundTwoDecimal(num: number): string {
const fNum = Number(num);
return (Math.round(fNum * 100) / 100).toFixed(2);
}
4 changes: 0 additions & 4 deletions packages/yoroi-extension/app/utils/nftMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ export function find721metadata(
return null;
}
const metadata = metadataWrapper['721'];
if (metadata.version && metadata.version !== '1.0') {
return null;
}

const assetName = Array.from(Buffer.from(assetNameHex, 'hex')).map(
c => String.fromCharCode(c)
).join('');
Expand Down
5 changes: 5 additions & 0 deletions packages/yoroi-extension/chrome/constants-mv2.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export function genCSP(request: {|
connectSrc.push('https://api.handle.me/');
connectSrc.push('https://api.unstoppabledomains.com/');

// Pool info
connectSrc.push('https://a.cexplorer.io/');
imgSrc.push('https://img.cexplorer.io/');
imgSrc.push('https://corsproxy.io/');

// wasm-eval is needed to compile WebAssembly in the browser
// note: wasm-eval is not standardized but empirically works in Firefox & Chrome https://github.com/w3c/webappsec-csp/pull/293
const evalSrc = "'wasm-eval'";
Expand Down
Loading

0 comments on commit 89dcea1

Please sign in to comment.