From ca905b322722e730540fd001e06f5daecd92331e Mon Sep 17 00:00:00 2001 From: Karim Dalaize Date: Thu, 22 Jun 2023 17:49:55 +0200 Subject: [PATCH 1/6] [Added] Millions messages, other denoms and prices handling --- package.json | 4 +- src/assets/images/messageTypes/claimPrize.svg | 3 + .../images/messageTypes/millionsDeposit.svg | 5 ++ .../images/messageTypes/millionsWithdraw.svg | 11 +++ src/assets/images/tokens/atom.svg | 10 +++ src/assets/images/tokens/dfr.svg | 16 ++++ src/assets/images/tokens/osmo.svg | 64 ++++++++++++++ src/assets/images/tokens/usdc.svg | 9 ++ src/assets/index.ts | 16 ++++ .../OtherAssetsTable/OtherAssetsTable.scss | 13 +++ .../OtherAssetsTable/OtherAssetsTable.tsx | 52 ++++++++++++ .../TransactionsTable.tsx | 20 ++++- src/components/index.ts | 3 +- src/constant/ibcDenoms.ts | 3 + src/constant/index.ts | 2 + src/index.tsx | 2 + src/locales/en.json | 14 +++- src/models/Metadata.ts | 27 ++++++ src/models/Token.ts | 14 ++++ src/models/index.ts | 13 ++- src/redux/models/stats.ts | 29 +++++++ src/redux/models/wallet.ts | 23 ++++- src/screens/Dashboard/Dashboard.tsx | 25 +++++- src/utils/client.ts | 14 +++- src/utils/denoms.ts | 19 +++++ src/utils/http.ts | 40 +++++++++ src/utils/index.ts | 2 + src/utils/links.ts | 10 ++- src/utils/numbers.ts | 16 +++- src/utils/statsApi.ts | 30 +++++++ src/utils/transactions.ts | 84 ++++++++++++++++++- yarn.lock | 18 +++- 32 files changed, 585 insertions(+), 26 deletions(-) create mode 100644 src/assets/images/messageTypes/claimPrize.svg create mode 100644 src/assets/images/messageTypes/millionsDeposit.svg create mode 100644 src/assets/images/messageTypes/millionsWithdraw.svg create mode 100644 src/assets/images/tokens/atom.svg create mode 100644 src/assets/images/tokens/dfr.svg create mode 100644 src/assets/images/tokens/osmo.svg create mode 100644 src/assets/images/tokens/usdc.svg create mode 100644 src/components/Tables/OtherAssetsTable/OtherAssetsTable.scss create mode 100644 src/components/Tables/OtherAssetsTable/OtherAssetsTable.tsx rename src/components/Tables/{ => TransactionsTable}/TransactionsTable.tsx (84%) create mode 100644 src/models/Metadata.ts create mode 100644 src/models/Token.ts create mode 100644 src/redux/models/stats.ts create mode 100644 src/utils/http.ts create mode 100644 src/utils/statsApi.ts diff --git a/package.json b/package.json index 0716028e..50a66a9b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@ledgerhq/hw-transport-webusb": "^6.7.0", - "@lum-network/sdk-javascript": "^0.8.4", + "@lum-network/sdk-javascript": "^0.8.5", "@popperjs/core": "^2.11.6", "@qognicafinance/react-lightweight-charts": "^1.0.5", "@rematch/core": "^2.2.0", @@ -15,6 +15,7 @@ "bootstrap": "^5.2.3", "bs58": "^5.0.0", "buffer": "^6.0.3", + "class-transformer": "^0.5.1", "clipboard": "^2.0.11", "crypto-browserify": "^3.12.0", "crypto-js": "^4.1.1", @@ -40,6 +41,7 @@ "react-toastify": "^9.1.1", "react-transition-group": "^4.4.2", "redux": "^4.0.5", + "reflect-metadata": "^0.1.13", "sass": "^1.52.1", "stream-browserify": "^3.0.0", "typescript": "^4.9.5", diff --git a/src/assets/images/messageTypes/claimPrize.svg b/src/assets/images/messageTypes/claimPrize.svg new file mode 100644 index 00000000..0070ae60 --- /dev/null +++ b/src/assets/images/messageTypes/claimPrize.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/messageTypes/millionsDeposit.svg b/src/assets/images/messageTypes/millionsDeposit.svg new file mode 100644 index 00000000..7814176f --- /dev/null +++ b/src/assets/images/messageTypes/millionsDeposit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/messageTypes/millionsWithdraw.svg b/src/assets/images/messageTypes/millionsWithdraw.svg new file mode 100644 index 00000000..62ee9bd8 --- /dev/null +++ b/src/assets/images/messageTypes/millionsWithdraw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/tokens/atom.svg b/src/assets/images/tokens/atom.svg new file mode 100644 index 00000000..e93efb3a --- /dev/null +++ b/src/assets/images/tokens/atom.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/tokens/dfr.svg b/src/assets/images/tokens/dfr.svg new file mode 100644 index 00000000..eb5274f4 --- /dev/null +++ b/src/assets/images/tokens/dfr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/images/tokens/osmo.svg b/src/assets/images/tokens/osmo.svg new file mode 100644 index 00000000..e74dcf1e --- /dev/null +++ b/src/assets/images/tokens/osmo.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/tokens/usdc.svg b/src/assets/images/tokens/usdc.svg new file mode 100644 index 00000000..212b70e0 --- /dev/null +++ b/src/assets/images/tokens/usdc.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/index.ts b/src/assets/index.ts index 780e8eaf..23928d42 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -19,6 +19,9 @@ import openBeam from './images/messageTypes/openBeam.svg'; import updateBeam from './images/messageTypes/updateBeam.svg'; import claimBeam from './images/messageTypes/claimBeam.svg'; import depositDfract from './images/messageTypes/dfract.svg'; +import depositMillions from './images/messageTypes/millionsDeposit.svg'; +import withdrawMillions from './images/messageTypes/millionsWithdraw.svg'; +import claimMillions from './images/messageTypes/claimPrize.svg'; import hardwareIcon from './images/hardware.svg'; import softwareIcon from './images/software.png'; import addIcon from './images/add.png'; @@ -57,6 +60,10 @@ import depositPeriod from './images/depositPeriod.svg'; import warningIcon from './images/warningIcon.svg'; import moreIcon from './images/more.svg'; import guardaIcon from './images/guarda.svg'; +import atomIcon from './images/tokens/atom.svg'; +import osmoIcon from './images/tokens/osmo.svg'; +import usdcIcon from './images/tokens/usdc.svg'; +import dfrIcon from './images/tokens/dfr.svg'; export default { images: { @@ -114,6 +121,9 @@ export default { claimBeam, setWithdrawAddress, depositDfract, + depositMillions, + withdrawMillions, + claimMillions, }, navbarIcons: { dashboard, @@ -123,5 +133,11 @@ export default { governance, logout, }, + tokens: { + atom: atomIcon, + dfr: dfrIcon, + osmo: osmoIcon, + usdc: usdcIcon, + }, }, }; diff --git a/src/components/Tables/OtherAssetsTable/OtherAssetsTable.scss b/src/components/Tables/OtherAssetsTable/OtherAssetsTable.scss new file mode 100644 index 00000000..5d7e0761 --- /dev/null +++ b/src/components/Tables/OtherAssetsTable/OtherAssetsTable.scss @@ -0,0 +1,13 @@ +@import 'src/styles/main'; + +.denom { + font-weight: bold; +} + +.usd-price { + color: #585858; + + @media (prefers-color-scheme: dark) { + color: $color-grey; + } +} \ No newline at end of file diff --git a/src/components/Tables/OtherAssetsTable/OtherAssetsTable.tsx b/src/components/Tables/OtherAssetsTable/OtherAssetsTable.tsx new file mode 100644 index 00000000..a6e0c465 --- /dev/null +++ b/src/components/Tables/OtherAssetsTable/OtherAssetsTable.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import numeral from 'numeral'; + +import { Table } from 'frontend-elements'; +import { OtherBalance } from 'models'; +import { RootState } from 'redux/store'; +import { DenomsUtils, NumbersUtils } from 'utils'; + +import SmallerDecimal from '../../SmallerDecimal/SmallerDecimal'; + +import './OtherAssetsTable.scss'; + +const OtherAssetsTable = ({ otherBalances }: { otherBalances: OtherBalance[] }) => { + const { t } = useTranslation(); + + const prices = useSelector((state: RootState) => state.stats.prices); + + const headers = t('dashboard.otherBalancesTable.headers', { returnObjects: true }); + + const renderRow = (otherBalance: OtherBalance) => { + const icon = DenomsUtils.getIconFromDenom(otherBalance.denom); + const price = prices.find((p) => p.denom === otherBalance.denom); + + return ( + + +
+ denom icon + {otherBalance.denom.toUpperCase()} +
+ + +
+
+ + {otherBalance.denom.toUpperCase()} +
+
+ {price && numeral(otherBalance.amount * price.price).format('$0,0[.]00')} +
+
+ + + ); + }; + + return {otherBalances.map((bal) => renderRow(bal))}
; +}; + +export default OtherAssetsTable; diff --git a/src/components/Tables/TransactionsTable.tsx b/src/components/Tables/TransactionsTable/TransactionsTable.tsx similarity index 84% rename from src/components/Tables/TransactionsTable.tsx rename to src/components/Tables/TransactionsTable/TransactionsTable.tsx index cab98b84..b0ebee5f 100644 --- a/src/components/Tables/TransactionsTable.tsx +++ b/src/components/Tables/TransactionsTable/TransactionsTable.tsx @@ -5,7 +5,7 @@ import { LumConstants } from '@lum-network/sdk-javascript'; import { Table } from 'frontend-elements'; import { Transaction, Wallet } from 'models'; -import { getExplorerLink, NumbersUtils, DenomsUtils, trunc } from 'utils'; +import { getExplorerLink, NumbersUtils, DenomsUtils, trunc, getMillionsLink } from 'utils'; import { SmallerDecimal, TransactionTypeBadge } from 'components'; import assets from 'assets'; @@ -70,6 +70,24 @@ const TransactionRow = (props: RowProps): JSX.Element => { ) : row.messages.length > 1 ? ( '-' + ) : row.toAddress.startsWith(t('transactions.pool')) ? ( + + {row.toAddress} + + ) : row.toAddress.startsWith(t('transactions.proposal')) ? ( + + {row.toAddress} + ) : ( trunc(row.toAddress || '-') )} diff --git a/src/components/index.ts b/src/components/index.ts index d66c0c9b..cc916058 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,7 +4,8 @@ export { default as AirdropCard } from './Cards/AirdropCard'; export { default as LumPriceCard } from './Cards/LumPriceCard'; export { default as BalanceCard } from './Cards/BalanceCard'; export { default as Card } from './Cards/Card'; -export { default as TransactionsTable } from './Tables/TransactionsTable'; +export { default as TransactionsTable } from './Tables/TransactionsTable/TransactionsTable'; +export { default as OtherAssetsTable } from './Tables/OtherAssetsTable/OtherAssetsTable'; export { default as Input } from './Inputs/Input'; export { default as SwitchInput } from './Inputs/SwitchInput'; export { default as HdPathInput } from './Inputs/HdPathInput'; diff --git a/src/constant/ibcDenoms.ts b/src/constant/ibcDenoms.ts index 23ceb813..cd428412 100644 --- a/src/constant/ibcDenoms.ts +++ b/src/constant/ibcDenoms.ts @@ -1,3 +1,6 @@ export const enum IBCDenoms { USDC = 'ibc/05554A9BFDD28894D7F18F4C707AA0930D778751A437A9FE1F4684A3E1199728', + OSMO = 'ibc/47BD209179859CDE4A2806763D7189B6E6FE13A17880FE2B42DE1E6C1E329E23', + ATOM = 'ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477', + ATOM_TESTNET = 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2', } diff --git a/src/constant/index.ts b/src/constant/index.ts index fdbb0b18..07af9f7e 100644 --- a/src/constant/index.ts +++ b/src/constant/index.ts @@ -13,6 +13,8 @@ export const LUM_EXPLORER = 'https://explorer.lum.network'; export const LUM_WALLET = 'https://wallet.lum.network'; export const LUM_EXPLORER_TESTNET = 'https://explorer.testnet.lum.network'; export const LUM_WALLET_TESTNET = 'https://wallet.testnet.lum.network'; +export const LUM_MILLIONS = 'https://cosmosmillions.com'; +export const LUM_MILLIONS_TESTNET = 'https://testnet.cosmosmillions.com'; export const LUM_COINGECKO_ID = 'lum-network'; export const CLIENT_PRECISION = 1_000_000_000_000_000_000; diff --git a/src/index.tsx b/src/index.tsx index ba8069b5..7cb7ba13 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,5 @@ +import 'reflect-metadata'; + import React from 'react'; import { createRoot } from 'react-dom/client'; import reportWebVitals from './reportWebVitals'; diff --git a/src/locales/en.json b/src/locales/en.json index 5670fa81..9380b564 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -74,6 +74,10 @@ "latestTx": "Latest Transactions", "currentBalance": "Your balance", "availableBalance": "Available balance", + "otherBalancesTable": { + "title": "My Other Assets", + "headers": ["Asset", "Balance"] + }, "followTwitter": "Follow us on Twitter", "noTx": "No transactions yet" }, @@ -230,9 +234,15 @@ "ibcReceivePacket": "IBC Receive Packet", "exec": "Exec", "grant": "Grant", - "depositDfract": "DFract Deposit" + "depositDfract": "DFract Deposit", + "depositMillions": "Millions Deposit", + "claimMillions": "Millions Claim Prize", + "withdrawMillions": "Millions Leave Pool", + "depositRetryMillions": "Millions Deposit Retry", + "withdrawRetryMillions": "Millions Withdraw Retry" }, - "proposal": "Proposal #" + "proposal": "Proposal #", + "pool": "Pool #" }, "operations": { "types": { diff --git a/src/models/Metadata.ts b/src/models/Metadata.ts new file mode 100644 index 00000000..b205df11 --- /dev/null +++ b/src/models/Metadata.ts @@ -0,0 +1,27 @@ +import { Exclude, Expose } from 'class-transformer'; + +@Exclude() +export class MetadataModel { + @Expose({ name: 'has_next_page' }) + hasNextPage!: boolean; + + @Expose({ name: 'has_previous_page' }) + hasPreviousPage!: boolean; + + @Expose({ name: 'items_count' }) + itemsCount!: number; + + @Expose({ name: 'items_total' }) + itemsTotal!: number; + + @Expose() + limit!: number; + + @Expose() + page!: number; + + @Expose({ name: 'pages_total' }) + pagesTotal!: number; +} + +export default MetadataModel; diff --git a/src/models/Token.ts b/src/models/Token.ts new file mode 100644 index 00000000..d4f05fbe --- /dev/null +++ b/src/models/Token.ts @@ -0,0 +1,14 @@ +import { Exclude, Expose, Transform, Type } from 'class-transformer'; + +@Exclude() +class TokenModel { + @Expose({ name: 'current_price' }) + price!: number; + + @Expose({ name: 'symbol' }) + @Type(() => String) + @Transform(({ value }) => value.toString().toLowerCase()) + denom!: string; +} + +export default TokenModel; diff --git a/src/models/index.ts b/src/models/index.ts index 5394b612..f62d8547 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -2,23 +2,31 @@ import { LumTypes, LumWallet } from '@lum-network/sdk-javascript'; import { Proposal as BaseProposal } from '@lum-network/sdk-javascript/build/codec/cosmos/gov/v1/gov'; import { Validator } from '@lum-network/sdk-javascript/build/codec/cosmos/staking/v1beta1/staking'; import { Models } from '@rematch/core'; + import { governance } from '../redux/models/governance'; import { staking } from '../redux/models/staking'; import { wallet } from '../redux/models/wallet'; +import { stats } from '../redux/models/stats'; export interface RootModel extends Models { wallet: typeof wallet; staking: typeof staking; governance: typeof governance; + stats: typeof stats; } -export const reduxModels: RootModel = { wallet, staking, governance }; +export const reduxModels: RootModel = { wallet, staking, governance, stats }; export interface Wallet extends LumWallet { isExtensionImport?: boolean; isNanoS?: boolean; } +export interface OtherBalance { + denom: string; + amount: number; +} + export interface CommonTransactionProps { messages: string[]; hash: string; @@ -119,3 +127,6 @@ export interface LumInfo { name: number; previousDaysPrices: PreviousDayPrice[]; } + +export { default as MetadataModel } from './Metadata'; +export { default as TokenModel } from './Token'; diff --git a/src/redux/models/stats.ts b/src/redux/models/stats.ts new file mode 100644 index 00000000..0ef75359 --- /dev/null +++ b/src/redux/models/stats.ts @@ -0,0 +1,29 @@ +import { createModel } from '@rematch/core'; +import { RootModel, TokenModel } from 'models'; +import { StatsApi } from 'utils'; + +interface StatsState { + prices: TokenModel[]; +} + +export const stats = createModel()({ + name: 'stats', + state: { + prices: [], + } as StatsState, + reducers: { + setPrices(state, prices: TokenModel[]) { + return { + ...state, + prices, + }; + }, + }, + effects: (dispatch) => ({ + async getPrices() { + const [res] = await StatsApi.getPrices(); + + dispatch.stats.setPrices(res); + }, + }), +}); diff --git a/src/redux/models/wallet.ts b/src/redux/models/wallet.ts index c3e2823e..0efe4563 100644 --- a/src/redux/models/wallet.ts +++ b/src/redux/models/wallet.ts @@ -10,10 +10,20 @@ import { DeviceModelId } from '@ledgerhq/devices'; import { LUM_COINGECKO_ID } from 'constant'; import i18n from 'locales'; import { getRpcFromNode, getWalletLink, GuardaUtils, showErrorToast, showSuccessToast, WalletClient } from 'utils'; - -import { Airdrop, HardwareMethod, Proposal, Rewards, RootModel, Transaction, Vestings, Wallet } from '../../models'; import { LOGOUT } from 'redux/constants'; +import { + Airdrop, + HardwareMethod, + OtherBalance, + Proposal, + Rewards, + RootModel, + Transaction, + Vestings, + Wallet, +} from '../../models'; + interface SendPayload { to: string; from: Wallet; @@ -65,6 +75,7 @@ interface SetWalletDataPayload { fiat: number; lum: number; }; + otherBalances?: OtherBalance[]; rewards?: Rewards; vestings?: Vestings; airdrop?: Airdrop; @@ -82,6 +93,7 @@ interface WalletState { fiat: number; lum: number; }; + otherBalances: OtherBalance[]; transactions: Transaction[]; rewards: Rewards; vestings: Vestings | null; @@ -97,6 +109,7 @@ export const wallet = createModel()({ fiat: 0, lum: 0, }, + otherBalances: [], transactions: [], rewards: { rewards: [], @@ -128,6 +141,7 @@ export const wallet = createModel()({ ...state, rewards: data.rewards || state.rewards, currentBalance: data.currentBalance || state.currentBalance, + otherBalances: data.otherBalances || state.otherBalances, transactions: data.transactions || state.transactions, vestings: data.vestings || state.vestings, airdrop: data.airdrop || state.airdrop, @@ -145,8 +159,8 @@ export const wallet = createModel()({ const result = await WalletClient.getWalletBalance(address); if (result) { - const { currentBalance } = result; - dispatch.wallet.setWalletData({ currentBalance }); + const { currentBalance, otherBalances } = result; + dispatch.wallet.setWalletData({ currentBalance, otherBalances }); } }, async getTransactions(address: string) { @@ -179,6 +193,7 @@ export const wallet = createModel()({ }, async reloadWalletInfos(address: string) { await Promise.all([ + dispatch.stats.getPrices(), dispatch.wallet.getWalletBalance(address), dispatch.wallet.getTransactions(address), dispatch.wallet.getRewards(address), diff --git a/src/screens/Dashboard/Dashboard.tsx b/src/screens/Dashboard/Dashboard.tsx index ac519ea9..d860a4ce 100644 --- a/src/screens/Dashboard/Dashboard.tsx +++ b/src/screens/Dashboard/Dashboard.tsx @@ -5,7 +5,15 @@ import { LumConstants, LumUtils } from '@lum-network/sdk-javascript'; import { Card } from 'frontend-elements'; import { LUM_TWITTER } from 'constant'; -import { TransactionsTable, AddressCard, AvailableCard, LumPriceCard, BalanceCard, AirdropCard } from 'components'; +import { + TransactionsTable, + AddressCard, + AvailableCard, + LumPriceCard, + BalanceCard, + AirdropCard, + OtherAssetsTable, +} from 'components'; import { RootState } from 'redux/store'; import StakedCoinsCard from '../Staking/components/Cards/StakedCoinsCard'; @@ -15,11 +23,12 @@ import './styles/Dashboard.scss'; const Dashboard = (): JSX.Element => { // Redux hooks - const { transactions, balance, wallet, vestings, airdrop, stakedCoins, rewards } = useSelector( + const { transactions, balance, otherBalances, wallet, vestings, airdrop, stakedCoins, rewards } = useSelector( (state: RootState) => ({ loading: state.loading.global.loading, transactions: state.wallet.transactions, balance: state.wallet.currentBalance, + otherBalances: state.wallet.otherBalances, wallet: state.wallet.currentWallet, stakedCoins: state.staking.stakedCoins, vestings: state.wallet.vestings, @@ -95,6 +104,18 @@ const Dashboard = (): JSX.Element => { + {otherBalances.length > 0 && ( +
+
+ +
+

{t('dashboard.otherBalancesTable.title')}

+
+ +
+
+
+ )}
diff --git a/src/utils/client.ts b/src/utils/client.ts index 56a6bb37..51941561 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -4,7 +4,7 @@ import { ProposalStatus, VoteOption } from '@lum-network/sdk-javascript/build/co import { Window as KeplrWindow } from '@keplr-wallet/types'; import { PasswordStrengthType, PasswordStrength, Wallet, Proposal, LumInfo } from 'models'; -import { showErrorToast, showSuccessToast } from 'utils'; +import { DenomsUtils, NumbersUtils, showErrorToast, showSuccessToast } from 'utils'; import i18n from 'locales'; import { COINGECKO_API_URL, IPFS_GATEWAY, MessageTypes } from 'constant'; @@ -267,13 +267,24 @@ class WalletClient { let lum = 0; let fiat = 0; + const otherBalancesArr = []; + const balances = await this.lumClient.getAllBalances(address); + const ulumBalance = balances.find((balance) => balance.denom === LumConstants.MicroLumDenom); + const otherBalances = balances.filter((balance) => balance.denom !== LumConstants.MicroLumDenom); if (ulumBalance) { lum += Number(LumUtils.convertUnit(ulumBalance, LumConstants.LumDenom)); } + for (const oB of otherBalances) { + otherBalancesArr.push({ + denom: DenomsUtils.computeDenom(oB.denom), + amount: NumbersUtils.convertUnitNumber(oB.amount), + }); + } + if (this.lumInfos) { fiat = lum * this.lumInfos.price; } @@ -283,6 +294,7 @@ class WalletClient { lum, fiat, }, + otherBalances: otherBalancesArr, }; }; diff --git a/src/utils/denoms.ts b/src/utils/denoms.ts index de24d9d2..64a022b3 100644 --- a/src/utils/denoms.ts +++ b/src/utils/denoms.ts @@ -1,12 +1,31 @@ +import assets from 'assets'; import { IBCDenoms } from 'constant'; export const computeDenom = (denom: string): string => { switch (denom) { case IBCDenoms.USDC: return 'usdc'; + case IBCDenoms.ATOM_TESTNET: + case IBCDenoms.ATOM: + return 'atom'; + case IBCDenoms.OSMO: + return 'osmo'; case 'udfr': return 'dfr'; default: return 'lum'; } }; + +export const getIconFromDenom = (denom: string) => { + switch (denom) { + case 'atom': + return assets.images.tokens.atom; + case 'osmo': + return assets.images.tokens.osmo; + case 'dfr': + return assets.images.tokens.dfr; + case 'usdc': + return assets.images.tokens.usdc; + } +}; diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 00000000..c0c5426e --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,40 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { plainToInstance } from 'class-transformer'; +import { MetadataModel } from 'models'; + +declare module 'axios' { + interface AxiosResponse extends Promise { + data: T; + } +} + +abstract class HttpClient { + protected readonly instance: AxiosInstance; + private readonly subObject?: string; + + protected constructor(baseURL: string, subObject?: string) { + this.instance = axios.create({ baseURL, timeout: 10000 }); + this.subObject = subObject; + + this.initializeResponseInterceptor(); + } + + private initializeResponseInterceptor = () => { + this.instance.interceptors.response.use((res) => this.handleResponse(res), this.handleError); + }; + + private handleResponse = ({ data }: AxiosResponse) => data; + + private handleError = (error: any): Promise => Promise.reject(error); + + protected request = async (config: AxiosRequestConfig, Model: any): Promise<[T, MetadataModel]> => { + const response = await this.instance.request(config); + + return [ + plainToInstance(Model, this.subObject ? response[this.subObject] : response) as unknown as T, + plainToInstance(MetadataModel, response.metadata), + ]; + }; +} + +export default HttpClient; diff --git a/src/utils/index.ts b/src/utils/index.ts index e55588ab..6ccc99a5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +export { default as HttpClient } from './http'; +export { default as StatsApi } from './statsApi'; export { default as WalletClient } from './client'; export type { MnemonicLength } from './client'; export * as WalletUtils from './client'; diff --git a/src/utils/links.ts b/src/utils/links.ts index 5b7c5c18..8b67a5b7 100644 --- a/src/utils/links.ts +++ b/src/utils/links.ts @@ -1,5 +1,12 @@ import WalletClient from './client'; -import { LUM_EXPLORER, LUM_EXPLORER_TESTNET, LUM_WALLET, LUM_WALLET_TESTNET } from 'constant'; +import { + LUM_EXPLORER, + LUM_EXPLORER_TESTNET, + LUM_MILLIONS, + LUM_MILLIONS_TESTNET, + LUM_WALLET, + LUM_WALLET_TESTNET, +} from 'constant'; const CUSTOM_NODE_KEY = 'custom-nodes'; @@ -24,3 +31,4 @@ export const getRpcFromNode = (node: string): string => `https://${node}/rpc`; export const getExplorerLink = (): string => (WalletClient.isTestnet() ? LUM_EXPLORER_TESTNET : LUM_EXPLORER); export const getWalletLink = (): string => (WalletClient.isTestnet() ? LUM_WALLET_TESTNET : LUM_WALLET); +export const getMillionsLink = (): string => (WalletClient.isTestnet() ? LUM_MILLIONS_TESTNET : LUM_MILLIONS); diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts index 4dcef43b..ade634f3 100644 --- a/src/utils/numbers.ts +++ b/src/utils/numbers.ts @@ -1,23 +1,31 @@ import { LumConstants, LumTypes, LumUtils } from '@lum-network/sdk-javascript'; import numeral from 'numeral'; -export const convertUnitNumber = (nb: number | string): number => { +export const convertUnitNumber = ( + nb: number | string, + fromDenom = LumConstants.MicroLumDenom, + toDenom = LumConstants.LumDenom, +): number => { let amount: string; + if (!nb) { + return 0; + } + if (typeof nb === 'string') { const split = nb.split('.'); amount = split[0]; } else { - amount = nb.toFixed(); + amount = nb.toFixed(fromDenom.startsWith('u') ? 0 : 6); } const coin = { amount, - denom: LumConstants.MicroLumDenom, + denom: fromDenom, }; - return parseFloat(LumUtils.convertUnit(coin, LumConstants.LumDenom)); + return parseFloat(LumUtils.convertUnit(coin, toDenom)); }; export const formatUnit = (coin: LumTypes.Coin, moreDecimal?: boolean): string => { diff --git a/src/utils/statsApi.ts b/src/utils/statsApi.ts new file mode 100644 index 00000000..78e1d1f1 --- /dev/null +++ b/src/utils/statsApi.ts @@ -0,0 +1,30 @@ +import { HttpClient } from 'utils'; +import { COINGECKO_API_URL } from 'constant'; +import { TokenModel } from 'models'; + +class StatsApi extends HttpClient { + private static instance?: StatsApi; + + private constructor() { + super(COINGECKO_API_URL); + } + + public static getInstance(): StatsApi { + if (!this.instance) { + this.instance = new StatsApi(); + } + + return this.instance; + } + + getPrices = async () => + this.request( + { + url: '/coins/markets?vs_currency=usd&ids=lum-network%2C%20cosmos%2C%20osmosis&order=id_asc', + method: 'GET', + }, + TokenModel, + ); +} + +export default StatsApi.getInstance(); diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 5294e3d7..bc32b8bd 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -37,6 +37,21 @@ type DFractInfos = { amount: LumTypes.Coin; }; +type MillionsDepositInfos = { + depositorAddress: string; + poolId: Long; + amount: LumTypes.Coin; + isSponsor: boolean; + winnerAddress: string; +}; + +type MillionsLeavePoolInfo = { + depositId: Long; + poolId: Long; + toAddress: string; + depositorAddress: string; +}; + export const isSendTxInfo = ( info: { fromAddress?: string; @@ -91,6 +106,36 @@ export const isDfractInfo = ( return !!(info && info.depositorAddress && info.amount); }; +export const isMillionsDepositInfo = ( + info: { + depositorAddress?: string; + poolId?: Long; + amount?: LumTypes.Coin; + isSponsor?: boolean; + winnerAddress?: string; + } | null, +): info is MillionsDepositInfos => { + return !!( + info && + info.depositorAddress && + info.amount && + info.poolId && + info.winnerAddress && + info.isSponsor !== undefined + ); +}; + +export const isMillionsLeavePoolInfo = ( + info: { + depositId?: Long; + poolId?: Long; + toAddress?: string; + depositorAddress?: string; + } | null, +): info is MillionsLeavePoolInfo => { + return !!(info && info.depositorAddress && info.depositId && info.poolId && info.toAddress); +}; + export const hashExists = (txs: Transaction[], hash: string): boolean => txs.findIndex((tx) => tx.hash === hash) > -1; export const formatTxs = (rawTxs: readonly TxResponse[] | TxResponse[]): Transaction[] => { @@ -125,10 +170,7 @@ export const formatTxs = (rawTxs: readonly TxResponse[] | TxResponse[]): Transac if (typeof txInfos === 'object') { tx.messages.push(msg.typeUrl); - if (isDfractInfo(txInfos)) { - tx.fromAddress = txInfos.depositorAddress; - tx.amount.push(txInfos.amount); - } else if (isSendTxInfo(txInfos)) { + if (isSendTxInfo(txInfos)) { tx.fromAddress = txInfos.fromAddress; tx.toAddress = txInfos.toAddress; tx.amount = txInfos.amount; @@ -148,6 +190,15 @@ export const formatTxs = (rawTxs: readonly TxResponse[] | TxResponse[]): Transac tx.fromAddress = txInfos.sender; tx.toAddress = txInfos.receiver; tx.amount.push(txInfos.token); + } else if (isMillionsDepositInfo(txInfos)) { + tx.fromAddress = txInfos.depositorAddress; + tx.toAddress = i18n.t('transactions.pool') + txInfos.poolId.toString(); + tx.amount.push(txInfos.amount); + } else if (isMillionsLeavePoolInfo(txInfos)) { + tx.fromAddress = i18n.t('transactions.pool') + txInfos.poolId.toString(); + } else if (isDfractInfo(txInfos)) { + tx.fromAddress = txInfos.depositorAddress; + tx.amount.push(txInfos.amount); } } } catch {} @@ -198,6 +249,31 @@ export const getTxTypeInfos = ( return { name: i18n.t('transactions.types.vote'), icon: assets.images.messageTypes.vote }; case LumMessages.MsgDepositDfractUrl: return { name: i18n.t('transactions.types.depositDfract'), icon: assets.images.messageTypes.depositDfract }; + case LumMessages.MsgMillionsDepositUrl: + return { + name: i18n.t('transactions.types.depositMillions'), + icon: assets.images.messageTypes.depositMillions, + }; + case LumMessages.MsgDepositRetryUrl: + return { + name: i18n.t('transactions.types.depositRetryMillions'), + icon: assets.images.messageTypes.depositMillions, + }; + case LumMessages.MsgClaimPrizeUrl: + return { + name: i18n.t('transactions.types.claimMillions'), + icon: assets.images.messageTypes.claimMillions, + }; + case LumMessages.MsgWithdrawDepositUrl: + return { + name: i18n.t('transactions.types.withdrawMillions'), + icon: assets.images.messageTypes.withdrawMillions, + }; + case LumMessages.MsgWithdrawDepositRetryUrl: + return { + name: i18n.t('transactions.types.withdrawRetryMillions'), + icon: assets.images.messageTypes.withdrawMillions, + }; case MessageTypes.IBC_TRANSFER: return { name: i18n.t('transactions.types.ibcTransfer'), icon: assets.images.messageTypes.beam }; case MessageTypes.IBC_TIMEOUT: diff --git a/yarn.lock b/yarn.lock index 1f7d0c3b..f67141ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2406,10 +2406,10 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@lum-network/sdk-javascript@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@lum-network/sdk-javascript/-/sdk-javascript-0.8.4.tgz#732d717d926e9f4673cc9968c9d527b77da25851" - integrity sha512-cRiOLyfTTK75OlQ2L330Xc1FuWEhGxBgBP6BnVoAmvgaeGdAjFGMZhS1SvYYIQvUwbnyPRi1DVKrVK2kFg3tuw== +"@lum-network/sdk-javascript@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@lum-network/sdk-javascript/-/sdk-javascript-0.8.5.tgz#949011d11edf3fa22e52ba2d911df008f20b6463" + integrity sha512-mKrDUq+VwhoG6BKwtmBcwIX5RNiQDyYBa+YrufGNZmvoMtBXtvRsXENFN/isIRXTiJRG/S3+3/A6aGrRyzrCtw== dependencies: "@cosmjs/amino" "0.30.1" "@cosmjs/crypto" "0.30.1" @@ -4481,6 +4481,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + clean-css@^5.2.2: version "5.3.2" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" @@ -9700,6 +9705,11 @@ redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" From 4c6148e08b13df3c84317cb0db6591cc54de166d Mon Sep 17 00:00:00 2001 From: Karim Dalaize Date: Thu, 29 Jun 2023 17:59:10 +0200 Subject: [PATCH 2/6] =?UTF-8?q?[Added]=C2=A0Other=20staking=20rewards=20ha?= =?UTF-8?q?ndling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Cards/BalanceCard.tsx | 5 +- src/locales/en.json | 9 +- src/redux/models/wallet.ts | 101 +++++++++++- .../components/Forms/GetAllRewards.tsx | 3 +- .../components/Forms/GetOtherRewards.tsx | 86 ++++++++++ src/screens/Staking/Staking.tsx | 105 +++++++++--- .../Staking/components/Cards/RewardsCard.tsx | 3 +- .../components/Lists/OtherStakingRewards.tsx | 82 ++++++++++ .../components/Lists/styles/Lists.scss | 153 +++++++++++------- src/utils/client.ts | 2 +- 10 files changed, 451 insertions(+), 98 deletions(-) create mode 100644 src/screens/Operations/components/Forms/GetOtherRewards.tsx create mode 100644 src/screens/Staking/components/Lists/OtherStakingRewards.tsx diff --git a/src/components/Cards/BalanceCard.tsx b/src/components/Cards/BalanceCard.tsx index 93e81cb9..b996ec2c 100644 --- a/src/components/Cards/BalanceCard.tsx +++ b/src/components/Cards/BalanceCard.tsx @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'; import { NumbersUtils, WalletClient } from 'utils'; import { SmallerDecimal } from 'components'; import { Rewards } from 'models'; -import { CLIENT_PRECISION } from 'constant'; interface Props { balance: number; @@ -18,9 +17,7 @@ const BalanceCard = ({ balance, rewards }: Props): JSX.Element => { const lumAmount = balance + - (rewards.total && rewards.total.length > 0 - ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) / CLIENT_PRECISION - : 0); + (rewards.total && rewards.total.length > 0 ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) : 0); const fiatAmount = lumAmount * (WalletClient.getLumInfos()?.price || 0); return ( diff --git a/src/locales/en.json b/src/locales/en.json index 9380b564..ddb9d37f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -269,6 +269,9 @@ "getAllRewards": { "name": "Get All Rewards" }, + "getOtherRewards": { + "name": "Get Other Rewards" + }, "vote": { "name": "Vote", "description": "Vote on a governance proposal" @@ -337,6 +340,9 @@ "unbondedTokens": "Unbonding Tokens", "vestedTokens": "Vesting Tokens", "rewards": "Rewards", + "otherStakingRewards": { + "title": "Other staking rewards" + }, "myValidators": { "title": "My validators", "empty": "No delegations yet" @@ -354,7 +360,8 @@ "votingPower": "Voting Power", "commission": "Commission", "stakedCoins": "Staked Coins", - "rewards": "Rewards" + "rewards": "Rewards", + "token": "Token" }, "status": ["Unspecified", "Unbonded", "Unbonding", "Bonded", "Jailed"], "claim": "Claim", diff --git a/src/redux/models/wallet.ts b/src/redux/models/wallet.ts index 0efe4563..63775970 100644 --- a/src/redux/models/wallet.ts +++ b/src/redux/models/wallet.ts @@ -7,9 +7,18 @@ import { VoteOption } from '@lum-network/sdk-javascript/build/codec/cosmos/gov/v import TransportWebUsb from '@ledgerhq/hw-transport-webusb'; import { DeviceModelId } from '@ledgerhq/devices'; -import { LUM_COINGECKO_ID } from 'constant'; +import { CLIENT_PRECISION, LUM_COINGECKO_ID } from 'constant'; import i18n from 'locales'; -import { getRpcFromNode, getWalletLink, GuardaUtils, showErrorToast, showSuccessToast, WalletClient } from 'utils'; +import { + DenomsUtils, + getRpcFromNode, + getWalletLink, + GuardaUtils, + NumbersUtils, + showErrorToast, + showSuccessToast, + WalletClient, +} from 'utils'; import { LOGOUT } from 'redux/constants'; import { @@ -44,7 +53,7 @@ interface GetRewardPayload { memo: string; } -interface GetAllRewardsPayload { +interface GetRewardsFromValidatorsPayload { validatorsAddresses: string[]; from: Wallet; memo: string; @@ -77,6 +86,7 @@ interface SetWalletDataPayload { }; otherBalances?: OtherBalance[]; rewards?: Rewards; + otherRewards?: Rewards[]; vestings?: Vestings; airdrop?: Airdrop; } @@ -96,6 +106,7 @@ interface WalletState { otherBalances: OtherBalance[]; transactions: Transaction[]; rewards: Rewards; + otherRewards: Rewards[]; vestings: Vestings | null; airdrop: Airdrop | null; currentNode: string; @@ -115,6 +126,7 @@ export const wallet = createModel()({ rewards: [], total: [], }, + otherRewards: [], vestings: null, airdrop: null, currentNode: process.env.REACT_APP_RPC_URL || '', @@ -140,6 +152,7 @@ export const wallet = createModel()({ return { ...state, rewards: data.rewards || state.rewards, + otherRewards: data.otherRewards || state.otherRewards, currentBalance: data.currentBalance || state.currentBalance, otherBalances: data.otherBalances || state.otherBalances, transactions: data.transactions || state.transactions, @@ -171,10 +184,78 @@ export const wallet = createModel()({ } }, async getRewards(address: string) { - const rewards = await WalletClient.getRewards(address); + const res = await WalletClient.getRewards(address); + + if (res) { + const { rewards: r } = res; - if (rewards) { - dispatch.wallet.setWalletData({ rewards }); + const oRewards = []; + + for (let i = 0; i < r.length; i++) { + if (r[i].reward.findIndex((reward) => reward.denom !== LumConstants.MicroLumDenom) > -1) { + oRewards.push(r[i]); + r.splice(i, 1); + } + } + + const lumRewards = [...r]; + + const rewards = { + rewards: lumRewards, + total: [ + { + denom: LumConstants.MicroLumDenom, + amount: NumbersUtils.convertUnitNumber( + lumRewards.reduce( + (acc, r) => + NumbersUtils.convertUnitNumber(r.reward.length > 0 ? r.reward[0].amount : '0') / + CLIENT_PRECISION + + acc, + 0, + ), + LumConstants.LumDenom, + LumConstants.MicroLumDenom, + ).toFixed(), + }, + ], + }; + + const otherRewards: Rewards[] = []; + + for (const oR of oRewards) { + const existsInArrayIndex = otherRewards.findIndex( + (item) => + item.rewards.length > 0 && + item.rewards[0].reward.length > 0 && + item.rewards[0].reward[0].denom === (oR.reward[0].denom || ''), + ); + + if (oR.reward.length > 0) { + if (existsInArrayIndex > -1) { + otherRewards[existsInArrayIndex].rewards.push({ + ...oR, + }); + otherRewards[existsInArrayIndex].total[0].amount = NumbersUtils.convertUnitNumber( + NumbersUtils.convertUnitNumber(otherRewards[existsInArrayIndex].total[0].amount) / + CLIENT_PRECISION + + NumbersUtils.convertUnitNumber(oR.reward[0].amount) / CLIENT_PRECISION, + LumConstants.LumDenom, + LumConstants.MicroLumDenom, + ).toFixed(); + } else { + otherRewards.push({ + rewards: [oR], + total: [ + { + denom: DenomsUtils.computeDenom(oR.reward[0].denom), + amount: oR.reward[0].amount, + }, + ], + }); + } + } + } + dispatch.wallet.setWalletData({ rewards, otherRewards }); } }, async getVestings(address: string) { @@ -438,8 +519,12 @@ export const wallet = createModel()({ dispatch.wallet.reloadWalletInfos(payload.from.getAddress()); return result; }, - async getAllRewards(payload: GetAllRewardsPayload) { - const result = await WalletClient.getAllRewards(payload.from, payload.validatorsAddresses, payload.memo); + async getRewardsFromValidators(payload: GetRewardsFromValidatorsPayload) { + const result = await WalletClient.getRewardsFromValidators( + payload.from, + payload.validatorsAddresses, + payload.memo, + ); if (!result) { return null; diff --git a/src/screens/Operations/components/Forms/GetAllRewards.tsx b/src/screens/Operations/components/Forms/GetAllRewards.tsx index d2aff372..3eb069e0 100644 --- a/src/screens/Operations/components/Forms/GetAllRewards.tsx +++ b/src/screens/Operations/components/Forms/GetAllRewards.tsx @@ -1,5 +1,4 @@ import { Input, Button as CustomButton } from 'components'; -import { CLIENT_PRECISION } from 'constant'; import { FormikContextType } from 'formik'; import { Button } from 'frontend-elements'; import { Rewards } from 'models'; @@ -28,7 +27,7 @@ const GetAllRewards = ({ form, isLoading, rewards }: Props): JSX.Element => { 0 - ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) / CLIENT_PRECISION + ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) : 0, )} readOnly diff --git a/src/screens/Operations/components/Forms/GetOtherRewards.tsx b/src/screens/Operations/components/Forms/GetOtherRewards.tsx new file mode 100644 index 00000000..c37f48bb --- /dev/null +++ b/src/screens/Operations/components/Forms/GetOtherRewards.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { Input, Button as CustomButton } from 'components'; +import { FormikContextType } from 'formik'; +import { Button } from 'frontend-elements'; +import { Rewards } from 'models'; +import { useTranslation } from 'react-i18next'; +import { NumbersUtils } from 'utils'; + +interface Props { + rewards: Rewards; + isLoading: boolean; + form: FormikContextType<{ + addresses: string; + memo: string; + }>; +} + +const GetOtherRewards = ({ form, isLoading, rewards }: Props): JSX.Element => { + const [confirming, setConfirming] = useState(false); + + const { t } = useTranslation(); + + return ( + <> + {confirming &&
{t('operations.confirmation')}
} +
+
+ 0 + ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) + : 0, + ) + + ' ' + + rewards.total[0].denom.toUpperCase() + } + readOnly + label={t('operations.inputs.rewards.label')} + /> +
+
+ {(!confirming || (confirming && form.values.memo)) && ( + + )} + {form.errors.memo &&

{form.errors.memo}

} +
+
+ + {confirming && ( + { + setConfirming(false); + }} + > + {t('operations.modify')} + + )} +
+
+ + ); +}; + +export default GetOtherRewards; diff --git a/src/screens/Staking/Staking.tsx b/src/screens/Staking/Staking.tsx index c0cb92c7..c2acd3ee 100644 --- a/src/screens/Staking/Staking.tsx +++ b/src/screens/Staking/Staking.tsx @@ -32,10 +32,13 @@ import Delegate from '../Operations/components/Forms/Delegate'; import Undelegate from '../Operations/components/Forms/Undelegate'; import GetReward from '../Operations/components/Forms/GetReward'; import GetAllRewards from '../Operations/components/Forms/GetAllRewards'; -import Redelegate from 'screens/Operations/components/Forms/Redelegate'; +import GetOtherRewards from '../Operations/components/Forms/GetOtherRewards'; +import Redelegate from '../Operations/components/Forms/Redelegate'; +import OtherStakingRewards from './components/Lists/OtherStakingRewards'; -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; +const noop = () => { + //do nothing +}; const Staking = (): JSX.Element => { // State @@ -44,20 +47,28 @@ const Staking = (): JSX.Element => { const [confirming, setConfirming] = useState(false); const [operationModal, setOperationModal] = useState(null); const [topValidatorConfirmationModal, setTopValidatorConfirmationModal] = useState(null); - const [onConfirmOperation, setOnConfirmOperation] = useState<() => void>(() => noop); + const [onConfirmOperation, setOnConfirmOperation] = useState<() => void>(noop); const [totalVotingPower, setTotalVotingPower] = useState(0); + const [otherRewardsToClaimIndex, setOtherRewardToClaimIndex] = useState(null); // Dispatch methods - const { getValidatorsInfos, delegate, redelegate, undelegate, getWalletInfos, getAllRewards, getReward } = - useRematchDispatch((dispatch: RootDispatch) => ({ - getValidatorsInfos: dispatch.staking.getValidatorsInfosAsync, - delegate: dispatch.wallet.delegate, - redelegate: dispatch.wallet.redelegate, - undelegate: dispatch.wallet.undelegate, - getWalletInfos: dispatch.wallet.reloadWalletInfos, - getReward: dispatch.wallet.getReward, - getAllRewards: dispatch.wallet.getAllRewards, - })); + const { + getValidatorsInfos, + delegate, + redelegate, + undelegate, + getWalletInfos, + getRewardsFromValidators, + getReward, + } = useRematchDispatch((dispatch: RootDispatch) => ({ + getValidatorsInfos: dispatch.staking.getValidatorsInfosAsync, + delegate: dispatch.wallet.delegate, + redelegate: dispatch.wallet.redelegate, + undelegate: dispatch.wallet.undelegate, + getWalletInfos: dispatch.wallet.reloadWalletInfos, + getReward: dispatch.wallet.getReward, + getRewardsFromValidators: dispatch.wallet.getRewardsFromValidators, + })); // Redux state values const { @@ -70,6 +81,7 @@ const Staking = (): JSX.Element => { wallet, vestings, rewards, + otherRewards, balance, delegations, unbondings, @@ -84,6 +96,7 @@ const Staking = (): JSX.Element => { vestings: state.wallet.vestings, balance: state.wallet.currentBalance, rewards: state.wallet.rewards, + otherRewards: state.wallet.otherRewards, bondedValidators: state.staking.validators.bonded, unbondedValidators: state.staking.validators.unbonded, unbondingValidators: state.staking.validators.unbonding, @@ -95,7 +108,7 @@ const Staking = (): JSX.Element => { loadingRedelegate: state.loading.effects.wallet.redelegate.loading, loadingUndelegate: state.loading.effects.wallet.undelegate.loading, loadingClaim: state.loading.effects.wallet.getReward.loading, - loadingClaimAll: state.loading.effects.wallet.getAllRewards.loading, + loadingClaimAll: state.loading.effects.wallet.getRewardsFromValidators.loading, })); const loadingAll = loadingDelegate || loadingUndelegate; @@ -176,6 +189,15 @@ const Staking = (): JSX.Element => { onSubmit: (values) => onSubmitGetAllRewards(values.memo), }); + const getOtherRewardsForm = useFormik({ + initialValues: { memo: '', addresses: '' }, + validationSchema: yup.object().shape({ + memo: yup.string(), + addresses: yup.string().required(), + }), + onSubmit: (values) => onSubmitGetOtherRewards(values.addresses, values.memo), + }); + // Effects useEffect(() => { if (wallet) { @@ -318,7 +340,25 @@ const Staking = (): JSX.Element => { }) .map((val) => val.operatorAddress); - const getAllRewardsResult = await getAllRewards({ + const getAllRewardsResult = await getRewardsFromValidators({ + from: wallet, + validatorsAddresses, + memo, + }); + + if (getAllRewardsResult) { + setTxResult({ hash: LumUtils.toHex(getAllRewardsResult.hash), error: getAllRewardsResult.error }); + } + } catch (e) { + showErrorToast((e as Error).message); + } + }; + + const onSubmitGetOtherRewards = async (addresses: string, memo: string) => { + try { + const validatorsAddresses = addresses.split(','); + + const getAllRewardsResult = await getRewardsFromValidators({ from: wallet, validatorsAddresses, memo, @@ -387,6 +427,19 @@ const Staking = (): JSX.Element => { } }; + const onClaimOther = (addresses: string, index: number) => { + setOtherRewardToClaimIndex(index); + if (operationModal) { + getOtherRewardsForm.setFieldValue('addresses', addresses).then(() => { + setModalType({ + id: LumMessages.MsgWithdrawDelegatorRewardUrl + '/others', + name: t('operations.types.getOtherRewards.name'), + }); + operationModal.show(); + }); + } + }; + // Rendering const renderModal = (): JSX.Element | null => { if (!modalType) { @@ -409,6 +462,15 @@ const Staking = (): JSX.Element => { case LumMessages.MsgWithdrawDelegatorRewardUrl + '/all': return ; + case LumMessages.MsgWithdrawDelegatorRewardUrl + '/others': + return ( + + ); + default: return
Soon
; } @@ -416,7 +478,7 @@ const Staking = (): JSX.Element => { return ( <> -
+
{airdrop && airdrop.amount > 0 ? ( @@ -465,6 +527,13 @@ const Staking = (): JSX.Element => { )}
+ {otherRewards.length > 0 && ( +
+ + + +
+ )}
{ className="logout-modal-cancel-btn me-sm-4 mb-4 mb-sm-0" data-bs-dismiss="modal" onClick={() => { - setOnConfirmOperation(() => noop); + setOnConfirmOperation(noop); }} >
{t('common.cancel')}
diff --git a/src/screens/Staking/components/Cards/RewardsCard.tsx b/src/screens/Staking/components/Cards/RewardsCard.tsx index e87243b8..16443cb4 100644 --- a/src/screens/Staking/components/Cards/RewardsCard.tsx +++ b/src/screens/Staking/components/Cards/RewardsCard.tsx @@ -1,7 +1,6 @@ import React from 'react'; import assets from 'assets'; -import { CLIENT_PRECISION } from 'constant'; import { Button, Card } from 'frontend-elements'; import { Rewards } from 'models'; import { useTranslation } from 'react-i18next'; @@ -27,7 +26,7 @@ const RewardsCard = ({ rewards, onClaim, isLoading }: Props): JSX.Element => { 0 - ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) / CLIENT_PRECISION + ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) : 0, )} big diff --git a/src/screens/Staking/components/Lists/OtherStakingRewards.tsx b/src/screens/Staking/components/Lists/OtherStakingRewards.tsx new file mode 100644 index 00000000..6cffa303 --- /dev/null +++ b/src/screens/Staking/components/Lists/OtherStakingRewards.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import numeral from 'numeral'; + +import { Table } from 'frontend-elements'; +import { Reward, Rewards } from 'models'; +import { DenomsUtils, NumbersUtils, trunc } from 'utils'; +import { SmallerDecimal } from 'components'; +import { RootState } from 'redux/store'; + +const OtherStakingRewards = ({ + otherRewards, + onClaim, +}: { + otherRewards: Rewards[]; + onClaim: (addresses: string, index: number) => void; +}) => { + const prices = useSelector((state: RootState) => state.stats.prices); + + const { t } = useTranslation(); + + const headers = [ + t('staking.tableLabels.validator'), + t('staking.tableLabels.token'), + t('staking.tableLabels.rewards'), + '', + ]; + + const onClaimPress = (rewards: Reward[], index: number) => { + const addresses = rewards.map((r) => r.validatorAddress).join(','); + + onClaim(addresses, index); + }; + + const renderRow = (rewards: Rewards, index: number) => { + const icon = DenomsUtils.getIconFromDenom(rewards.total[0].denom); + const price = prices.find((p) => p.denom === rewards.total[0].denom); + const amount = NumbersUtils.convertUnitNumber(rewards.total[0].amount); + + return ( + + {trunc(rewards.rewards[0].validatorAddress)} + +
+ {icon && denom icon} + {rewards.total[0].denom.toUpperCase()} +
+ + +
+
+ + {rewards.total[0].denom.toUpperCase()} +
+
{price && numeral(amount * price.price).format('$0,0[.]00')}
+
+ + + + + + ); + }; + + return ( + <> +
+

{t('staking.otherStakingRewards.title')}

+
+ {otherRewards.map(renderRow)}
+ + ); +}; + +export default OtherStakingRewards; diff --git a/src/screens/Staking/components/Lists/styles/Lists.scss b/src/screens/Staking/components/Lists/styles/Lists.scss index 28c44a71..6c0d0286 100644 --- a/src/screens/Staking/components/Lists/styles/Lists.scss +++ b/src/screens/Staking/components/Lists/styles/Lists.scss @@ -1,82 +1,111 @@ @import 'src/styles/main'; -.delegate-btn, -.delegate-btn:hover { - display: flex; - justify-content: center; - align-items: center; - padding: 5px 15px; - position: relative; - background: linear-gradient(white, white) padding-box, - linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; - border: 1px solid transparent; - color: $color-primary; -} +#staking { + .delegate-btn, + .delegate-btn:hover { + display: flex; + justify-content: center; + align-items: center; + padding: 5px 15px; + position: relative; + background: linear-gradient(white, white) padding-box, + linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; + border: 1px solid transparent; + color: $color-primary; + } -tr:nth-child(odd) > td > .delegate-btn { - background: linear-gradient($color-striped, $color-striped) padding-box, - linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; -} + tr:nth-child(odd) > td > .delegate-btn { + background: linear-gradient($color-striped, $color-striped) padding-box, + linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; + } -.validators-table { - overflow-y: visible !important; -} + .validators-table { + overflow-y: visible !important; + } -.validators-search-input { - width: 264px; -} + .validators-search-input { + width: 264px; + } -.search-input { - box-shadow: inset 0px 3.42928px 5.14392px rgba(0, 0, 0, 0.05); -} + .search-input { + box-shadow: inset 0px 3.42928px 5.14392px rgba(0, 0, 0, 0.05); + } -.search-input > input { - font-size: 10px; - margin-left: 0.5rem !important; + .search-input > input { + font-size: 10px; + margin-left: 0.5rem !important; + } + + .action-btn { + background-color: transparent; + border: 2px $color-primary solid; + color: $color-primary; + padding: 5px 15px; + &:disabled { + background-color: transparent; + color: $color-grey; + border: 2px $color-grey solid; + } + } } @include media-breakpoint-down(lg) { - .validators-table-row > td { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - .validators-table > table.table thead { - display: none; - } + #staking { + .validators-table-row > td { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .validators-table > table.table thead { + display: none; + } - .validators-table > table.table tr { - display: block; - margin: 12px 0; - } + .validators-table > table.table tr { + display: block; + margin: 12px 0; + } - .validators-table > table.table td { - display: block; - text-align: right; - height: 40px; - padding-left: 24px !important; - padding-right: 24px !important; - } + .validators-table > table.table td { + display: block; + text-align: right; + height: 40px; + padding-left: 24px !important; + padding-right: 24px !important; + } - .validators-table > table.table td:before { - content: attr(data-label); - float: left; - font-weight: $medium; + .validators-table > table.table td:before { + content: attr(data-label); + float: left; + font-weight: $medium; + } } } @media (prefers-color-scheme: dark) { - .validator-logo { - filter: brightness(0) invert(1); - } - .delegate-btn, - .delegate-btn:hover { - background: linear-gradient($color-primary, $color-primary) padding-box, - linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; - color: white; - } + #staking { + .validator-logo { + filter: brightness(0) invert(1); + } + .delegate-btn, + .delegate-btn:hover { + background: linear-gradient($color-primary, $color-primary) padding-box, + linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; + color: white; + } - tr:nth-child(odd) > td > .delegate-btn { - background: linear-gradient($color-dark-striped, $color-dark-striped) padding-box, - linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; + tr:nth-child(odd) > td > .delegate-btn { + background: linear-gradient($color-dark-striped, $color-dark-striped) padding-box, + linear-gradient(to top, rgba(192, 113, 79, 1), rgba(253, 184, 134, 1)) border-box; + } + + .action-btn { + border: 2px white solid; + color: white; + + &:disabled { + background-color: transparent; + color: $color-grey; + border: 2px $color-grey solid; + } + } } } diff --git a/src/utils/client.ts b/src/utils/client.ts index 51941561..f3793b6a 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -686,7 +686,7 @@ class WalletClient { }; }; - getAllRewards = async (fromWallet: Wallet, validatorsAddresses: string[], memo: string) => { + getRewardsFromValidators = async (fromWallet: Wallet, validatorsAddresses: string[], memo: string) => { if (this.lumClient === null) { return null; } From bbe9d8d9c8c5dbd7c15547554c981cffc98c6230 Mon Sep 17 00:00:00 2001 From: Karim Dalaize Date: Thu, 29 Jun 2023 18:27:42 +0200 Subject: [PATCH 3/6] [Updated] total calculation --- src/redux/models/wallet.ts | 29 ++++++++++++------- .../components/Lists/OtherStakingRewards.tsx | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/redux/models/wallet.ts b/src/redux/models/wallet.ts index 63775970..68113aa1 100644 --- a/src/redux/models/wallet.ts +++ b/src/redux/models/wallet.ts @@ -3,6 +3,7 @@ import { createModel } from '@rematch/core'; import { Window as KeplrWindow } from '@keplr-wallet/types'; import { LumUtils, LumWalletFactory, LumWallet, LumConstants } from '@lum-network/sdk-javascript'; import { VoteOption } from '@lum-network/sdk-javascript/build/codec/cosmos/gov/v1beta1/gov'; +import { DelegationDelegatorReward } from '@lum-network/sdk-javascript/build/codec/cosmos/distribution/v1beta1/distribution'; import TransportWebUsb from '@ledgerhq/hw-transport-webusb'; import { DeviceModelId } from '@ledgerhq/devices'; @@ -189,16 +190,16 @@ export const wallet = createModel()({ if (res) { const { rewards: r } = res; - const oRewards = []; + const oRewards: DelegationDelegatorReward[] = []; - for (let i = 0; i < r.length; i++) { - if (r[i].reward.findIndex((reward) => reward.denom !== LumConstants.MicroLumDenom) > -1) { - oRewards.push(r[i]); - r.splice(i, 1); + const lumRewards = r.filter((reward) => { + if (reward.reward.findIndex((reward) => reward.denom !== LumConstants.MicroLumDenom) > -1) { + oRewards.push(reward); + return false; } - } - const lumRewards = [...r]; + return true; + }); const rewards = { rewards: lumRewards, @@ -232,13 +233,19 @@ export const wallet = createModel()({ if (oR.reward.length > 0) { if (existsInArrayIndex > -1) { + const oldTotal = NumbersUtils.convertUnitNumber( + otherRewards[existsInArrayIndex].total[0].amount, + ); + const rewardAmount = NumbersUtils.convertUnitNumber( + parseFloat(oR.reward[0].amount) / CLIENT_PRECISION, + ); + otherRewards[existsInArrayIndex].rewards.push({ ...oR, }); + otherRewards[existsInArrayIndex].total[0].amount = NumbersUtils.convertUnitNumber( - NumbersUtils.convertUnitNumber(otherRewards[existsInArrayIndex].total[0].amount) / - CLIENT_PRECISION + - NumbersUtils.convertUnitNumber(oR.reward[0].amount) / CLIENT_PRECISION, + oldTotal + rewardAmount, LumConstants.LumDenom, LumConstants.MicroLumDenom, ).toFixed(); @@ -248,7 +255,7 @@ export const wallet = createModel()({ total: [ { denom: DenomsUtils.computeDenom(oR.reward[0].denom), - amount: oR.reward[0].amount, + amount: (parseFloat(oR.reward[0].amount) / CLIENT_PRECISION).toFixed(), }, ], }); diff --git a/src/screens/Staking/components/Lists/OtherStakingRewards.tsx b/src/screens/Staking/components/Lists/OtherStakingRewards.tsx index 6cffa303..72a7826a 100644 --- a/src/screens/Staking/components/Lists/OtherStakingRewards.tsx +++ b/src/screens/Staking/components/Lists/OtherStakingRewards.tsx @@ -39,7 +39,7 @@ const OtherStakingRewards = ({ const amount = NumbersUtils.convertUnitNumber(rewards.total[0].amount); return ( - + {trunc(rewards.rewards[0].validatorAddress)}
From e5b48f4b78628c16446c222b2633b47c6fc8e53d Mon Sep 17 00:00:00 2001 From: Karim Dalaize Date: Thu, 27 Jul 2023 16:03:51 +0200 Subject: [PATCH 4/6] =?UTF-8?q?[Updated]=C2=A0Other=20rewards=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/images/arrow.svg | 3 + src/assets/index.ts | 2 + src/components/Collapsible/Collapsible.scss | 83 +++++++++++ src/components/Collapsible/Collapsible.tsx | 111 +++++++++++++++ src/components/index.ts | 1 + src/locales/en.json | 4 +- src/redux/models/wallet.ts | 7 +- .../components/Forms/GetOtherRewards.tsx | 86 ------------ src/screens/Staking/Staking.tsx | 80 ++--------- .../components/Lists/OtherStakingRewards.tsx | 131 +++++++++--------- .../components/Lists/styles/Lists.scss | 9 ++ src/utils/staking.ts | 40 ++++-- 12 files changed, 320 insertions(+), 237 deletions(-) create mode 100644 src/assets/images/arrow.svg create mode 100644 src/components/Collapsible/Collapsible.scss create mode 100644 src/components/Collapsible/Collapsible.tsx delete mode 100644 src/screens/Operations/components/Forms/GetOtherRewards.tsx diff --git a/src/assets/images/arrow.svg b/src/assets/images/arrow.svg new file mode 100644 index 00000000..bbc821ad --- /dev/null +++ b/src/assets/images/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/index.ts b/src/assets/index.ts index 23928d42..ed38712f 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -64,9 +64,11 @@ import atomIcon from './images/tokens/atom.svg'; import osmoIcon from './images/tokens/osmo.svg'; import usdcIcon from './images/tokens/usdc.svg'; import dfrIcon from './images/tokens/dfr.svg'; +import arrow from './images/arrow.svg'; export default { images: { + arrow, airdropCoins, hardwareIcon, softwareIcon, diff --git a/src/components/Collapsible/Collapsible.scss b/src/components/Collapsible/Collapsible.scss new file mode 100644 index 00000000..4da1bd8f --- /dev/null +++ b/src/components/Collapsible/Collapsible.scss @@ -0,0 +1,83 @@ +@import 'src/styles/main'; + +.collapsible { + padding: 20px 24px; + + .collapsible-title { + width: 100%; + font-style: normal; + font-weight: 400; + font-size: 18px; + line-height: 22px; + color: $color-black; + + display: flex; + justify-content: space-between; + align-items: center; + + position: relative; + + @include media-breakpoint-down(md) { + align-items: stretch; + justify-content: flex-start; + flex-direction: column; + } + + @media (prefers-color-scheme: dark) { + .collapsible-arrow { + filter: brightness(0) invert(1); + } + } + } + + .collapsible-content { + margin-top: 24px; + color: $color-black; + font-style: normal; + font-weight: 300; + font-size: 16px; + line-height: 19px; + } + + .collapsible-btn { + cursor: pointer; + width: 40px; + height: 40px; + + position: absolute; + right: 0; + + img { + width: 10px; + transition: all 0.25s ease-out; + transform-origin: center; + transform: rotate(90deg); + } + + &.with-button-border { + border: 2px solid $color-primary; + padding: 8px 18px; + font-size: 14px; + line-height: 17px; + color: $color-primary; + width: unset; + + @media (prefers-color-scheme: dark) { + color: $color-white; + border-color: $color-white; + } + } + + @include media-breakpoint-down(md) { + bottom: 0; + } + + &.is-showing img { + transform: rotate(270deg); + } + } + + &.clickable { + cursor: pointer; + } +} diff --git a/src/components/Collapsible/Collapsible.tsx b/src/components/Collapsible/Collapsible.tsx new file mode 100644 index 00000000..f04a3e33 --- /dev/null +++ b/src/components/Collapsible/Collapsible.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Collapse } from 'bootstrap'; + +import Assets from 'assets'; + +import './Collapsible.scss'; + +interface Props { + header: string | JSX.Element; + content: string | JSX.Element; + id: string; + disabled?: boolean; + withButton?: boolean; + toggleWithButton?: boolean; + className?: string; + border?: boolean; + buttonBorder?: boolean; + onExpand?: () => void; + onCollapse?: () => void; +} + +const Collapsible = (props: Props) => { + const [isShowing, setIsShowing] = useState(false); + const [bsCollapse, setBsCollapse] = useState(); + const collapseRef = useRef(null); + + const { t } = useTranslation(); + + const { + header, + content, + id, + className, + toggleWithButton, + disabled, + border = true, + buttonBorder, + onCollapse, + onExpand, + } = props; + + useEffect(() => { + const collapsible = document.getElementById(id); + + if (collapsible) { + setBsCollapse(new Collapse(collapsible, { toggle: false })); + + collapsible.addEventListener('hide.bs.collapse', () => { + setIsShowing(false); + }); + collapsible.addEventListener('show.bs.collapse', () => { + setIsShowing(true); + }); + } + }, []); + + const onToggle = (show: boolean) => { + if (disabled) { + return; + } + + if (show) { + bsCollapse?.show(); + onExpand && onExpand(); + } else { + bsCollapse?.hide(); + onCollapse && onCollapse(); + } + }; + + return ( +
onToggle(!isShowing)} + className={`collapsible ${!toggleWithButton ? 'clickable' : ''} ${border ? 'with-border' : ''} ${ + isShowing && 'is-showing' + } ${className}`} + > +
+ {header} + {!disabled && ( +
onToggle(!isShowing) : undefined} + className={`collapsible-btn ${ + isShowing && 'is-showing' + } d-flex align-items-center justify-content-center ${ + buttonBorder ? 'with-button-border rounded-pill' : 'ms-4' + }`} + > + {buttonBorder && + (isShowing ? ( + + ) : ( + + ))} + arrow +
+ )} +
+
+ {typeof content === 'string' ? ( +

+ ) : ( + content + )} +

+
+ ); +}; + +export default Collapsible; diff --git a/src/components/index.ts b/src/components/index.ts index cc916058..f418f488 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -21,3 +21,4 @@ export { default as HoverTooltip } from './Tooltip/HoverTooltip'; export { default as SmallerDecimal } from './SmallerDecimal/SmallerDecimal'; export { default as TransactionTypeBadge } from './TransactionTypeBadge/TransactionTypeBadge'; export { default as Badge } from './Badge/Badge'; +export { default as Collapsible } from './Collapsible/Collapsible'; diff --git a/src/locales/en.json b/src/locales/en.json index ddb9d37f..52c287da 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -24,7 +24,9 @@ "reset": "Reset", "total": "Total", "day": "24h", - "more": "More" + "more": "More", + "openDetails": "Open details", + "closeDetails": "Close details" }, "navbar": { "dashboard": "Dashboard", diff --git a/src/redux/models/wallet.ts b/src/redux/models/wallet.ts index 02513144..8cdd8e67 100644 --- a/src/redux/models/wallet.ts +++ b/src/redux/models/wallet.ts @@ -251,8 +251,9 @@ export const wallet = createModel()({ const oldTotal = NumbersUtils.convertUnitNumber( otherRewards[existsInArrayIndex].total[0].amount, ); + const rewardAmount = NumbersUtils.convertUnitNumber( - NumbersUtils.convertUnitNumber(oR.reward[0].amount) / CLIENT_PRECISION, + parseFloat(oR.reward[0].amount) / CLIENT_PRECISION, ); otherRewards[existsInArrayIndex].rewards.push({ @@ -270,9 +271,7 @@ export const wallet = createModel()({ total: [ { denom: oR.reward[0].denom, - amount: ( - NumbersUtils.convertUnitNumber(oR.reward[0].amount) / CLIENT_PRECISION - ).toFixed(), + amount: (parseFloat(oR.reward[0].amount) / CLIENT_PRECISION).toFixed(), }, ], }); diff --git a/src/screens/Operations/components/Forms/GetOtherRewards.tsx b/src/screens/Operations/components/Forms/GetOtherRewards.tsx deleted file mode 100644 index c37f48bb..00000000 --- a/src/screens/Operations/components/Forms/GetOtherRewards.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useState } from 'react'; -import { Input, Button as CustomButton } from 'components'; -import { FormikContextType } from 'formik'; -import { Button } from 'frontend-elements'; -import { Rewards } from 'models'; -import { useTranslation } from 'react-i18next'; -import { NumbersUtils } from 'utils'; - -interface Props { - rewards: Rewards; - isLoading: boolean; - form: FormikContextType<{ - addresses: string; - memo: string; - }>; -} - -const GetOtherRewards = ({ form, isLoading, rewards }: Props): JSX.Element => { - const [confirming, setConfirming] = useState(false); - - const { t } = useTranslation(); - - return ( - <> - {confirming &&
{t('operations.confirmation')}
} -
-
- 0 - ? NumbersUtils.convertUnitNumber(rewards.total[0].amount) - : 0, - ) + - ' ' + - rewards.total[0].denom.toUpperCase() - } - readOnly - label={t('operations.inputs.rewards.label')} - /> -
-
- {(!confirming || (confirming && form.values.memo)) && ( - - )} - {form.errors.memo &&

{form.errors.memo}

} -
-
- - {confirming && ( - { - setConfirming(false); - }} - > - {t('operations.modify')} - - )} -
-
- - ); -}; - -export default GetOtherRewards; diff --git a/src/screens/Staking/Staking.tsx b/src/screens/Staking/Staking.tsx index da543b1a..8ebbe9fc 100644 --- a/src/screens/Staking/Staking.tsx +++ b/src/screens/Staking/Staking.tsx @@ -32,7 +32,6 @@ import Delegate from '../Operations/components/Forms/Delegate'; import Undelegate from '../Operations/components/Forms/Undelegate'; import GetReward from '../Operations/components/Forms/GetReward'; import GetAllRewards from '../Operations/components/Forms/GetAllRewards'; -import GetOtherRewards from '../Operations/components/Forms/GetOtherRewards'; import Redelegate from '../Operations/components/Forms/Redelegate'; import OtherStakingRewards from './components/Lists/OtherStakingRewards'; @@ -49,7 +48,6 @@ const Staking = (): JSX.Element => { const [topValidatorConfirmationModal, setTopValidatorConfirmationModal] = useState(null); const [onConfirmOperation, setOnConfirmOperation] = useState<() => void>(noop); const [totalVotingPower, setTotalVotingPower] = useState(0); - const [otherRewardsToClaimIndex, setOtherRewardToClaimIndex] = useState(null); // Dispatch methods const { @@ -189,15 +187,6 @@ const Staking = (): JSX.Element => { onSubmit: (values) => onSubmitGetAllRewards(values.memo), }); - const getOtherRewardsForm = useFormik({ - initialValues: { memo: '', addresses: '' }, - validationSchema: yup.object().shape({ - memo: yup.string(), - addresses: yup.string().required(), - }), - onSubmit: (values) => onSubmitGetOtherRewards(values.addresses, values.memo), - }); - // Effects useEffect(() => { if (wallet) { @@ -354,24 +343,6 @@ const Staking = (): JSX.Element => { } }; - const onSubmitGetOtherRewards = async (addresses: string, memo: string) => { - try { - const validatorsAddresses = addresses.split(','); - - const getAllRewardsResult = await getRewardsFromValidators({ - from: wallet, - validatorsAddresses, - memo, - }); - - if (getAllRewardsResult) { - setTxResult({ hash: LumUtils.toHex(getAllRewardsResult.hash), error: getAllRewardsResult.error }); - } - } catch (e) { - showErrorToast((e as Error).message); - } - }; - // Click methods const onDelegate = (validator: Validator, totalVotingPower: number, force = false) => { if (!force && NumbersUtils.convertUnitNumber(validator.tokens || 0) / totalVotingPower > 0.08) { @@ -427,19 +398,6 @@ const Staking = (): JSX.Element => { } }; - const onClaimOther = (addresses: string, index: number) => { - setOtherRewardToClaimIndex(index); - if (operationModal) { - getOtherRewardsForm.setFieldValue('addresses', addresses).then(() => { - setModalType({ - id: LumMessages.MsgWithdrawDelegatorRewardUrl + '/others', - name: t('operations.types.getOtherRewards.name'), - }); - operationModal.show(); - }); - } - }; - // Rendering const renderModal = (): JSX.Element | null => { if (!modalType) { @@ -462,15 +420,6 @@ const Staking = (): JSX.Element => { case LumMessages.MsgWithdrawDelegatorRewardUrl + '/all': return ; - case LumMessages.MsgWithdrawDelegatorRewardUrl + '/others': - return ( - - ); - default: return
Soon
; } @@ -527,6 +476,20 @@ const Staking = (): JSX.Element => { )}
+ {otherRewards.length > 0 && ( +
+ + + +
+ )}
{ />
- {otherRewards.length > 0 && ( -
- - - -
- )}
void; -}) => { +const OtherStakingRewards = ({ validators, otherRewards }: { validators: Validator[]; otherRewards: Rewards[] }) => { const prices = useSelector((state: RootState) => state.stats.prices); const { t } = useTranslation(); @@ -31,49 +23,34 @@ const OtherStakingRewards = ({ '', ]; - const onClaimPress = (reward: Reward, index: number) => { - //const addresses = rewards.map((r) => r.validatorAddress).join(','); - - onClaim(reward.validatorAddress, index); - }; - const renderRow = (rewards: Reward, index: number) => { const normalDenom = DenomsUtils.computeDenom(rewards.reward[0].denom); - const icon = DenomsUtils.getIconFromDenom(normalDenom); const price = prices.find((p) => p.denom === normalDenom); const amount = NumbersUtils.convertUnitNumber(parseFloat(rewards.reward[0].amount) / CLIENT_PRECISION); const validator = validators.find((val) => val.operatorAddress === rewards.validatorAddress); return ( - - - - - - {validator?.description?.moniker || - validator?.description?.identity || - trunc(rewards.validatorAddress)} - - - - -
- {icon && denom icon} - {DenomsUtils.computeDenom(rewards.reward[0].denom).toUpperCase()} -
- - +
+ + + + {validator?.description?.moniker || + validator?.description?.identity || + trunc(rewards.validatorAddress)} + + +
@@ -83,17 +60,45 @@ const OtherStakingRewards = ({
{price && numeral(amount * price.price).format('$0,0[.]00')}
- - - - - +
+
+ ); + }; + + const renderCollapsible = (rewards: Rewards, index: number) => { + const normalDenom = DenomsUtils.computeDenom(rewards.total[0].denom); + const price = prices.find((p) => p.denom === normalDenom); + const amount = NumbersUtils.convertUnitNumber(rewards.total[0].amount); + + return ( + + + +
+
+ + + {DenomsUtils.computeDenom(rewards.total[0].denom).toUpperCase()} + +
+
+ {price && numeral(amount * price.price).format('$0,0[.]00')} +
+
+
+ } + content={<>{rewards.rewards.map(renderRow)}} + /> +
); }; @@ -102,11 +107,7 @@ const OtherStakingRewards = ({

{t('staking.otherStakingRewards.title')}

- {otherRewards.map((oR) => ( - <> - {oR.rewards.map(renderRow)}
- - ))} + {otherRewards.map(renderCollapsible)} ); }; diff --git a/src/screens/Staking/components/Lists/styles/Lists.scss b/src/screens/Staking/components/Lists/styles/Lists.scss index 6c0d0286..6821b182 100644 --- a/src/screens/Staking/components/Lists/styles/Lists.scss +++ b/src/screens/Staking/components/Lists/styles/Lists.scss @@ -47,6 +47,11 @@ border: 2px $color-grey solid; } } + + .other-rewards-card { + box-shadow: none; + background-color: $color-striped; + } } @include media-breakpoint-down(lg) { @@ -107,5 +112,9 @@ border: 2px $color-grey solid; } } + + .other-rewards-card { + background-color: $color-dark-striped; + } } } diff --git a/src/utils/staking.ts b/src/utils/staking.ts index e22f907f..d9349f0d 100644 --- a/src/utils/staking.ts +++ b/src/utils/staking.ts @@ -53,21 +53,31 @@ export const getUserValidators = ( const validators = []; for (const delegation of delegations) { - for (const reward of rewards.rewards) { - if (delegation.delegation && reward.validatorAddress === delegation.delegation.validatorAddress) { - const validator = validatorsList.find( - (bondedVal) => - delegation.delegation && bondedVal.operatorAddress === delegation.delegation.validatorAddress, - ); - - if (validator) { - validators.push({ - ...validator, - reward: parseFloat(reward.reward.length > 0 ? reward.reward[0].amount : '0') / CLIENT_PRECISION, - stakedCoins: NumbersUtils.formatTo6digit( - NumbersUtils.convertUnitNumber(delegation.balance?.amount || '0'), - ), - }); + const validator = validatorsList.find( + (bondedVal) => + delegation.delegation && bondedVal.operatorAddress === delegation.delegation.validatorAddress, + ); + if (validator) { + if (rewards.rewards.length === 0) { + validators.push({ + ...validator, + reward: 0, + stakedCoins: NumbersUtils.formatTo6digit( + NumbersUtils.convertUnitNumber(delegation.balance?.amount || '0'), + ), + }); + } else { + for (const reward of rewards.rewards) { + if (delegation.delegation && reward.validatorAddress === delegation.delegation.validatorAddress) { + validators.push({ + ...validator, + reward: + parseFloat(reward.reward.length > 0 ? reward.reward[0].amount : '0') / CLIENT_PRECISION, + stakedCoins: NumbersUtils.formatTo6digit( + NumbersUtils.convertUnitNumber(delegation.balance?.amount || '0'), + ), + }); + } } } } From e3ec3a6b5537f67d7b40fc028d388f22d748f795 Mon Sep 17 00:00:00 2001 From: Karim Dalaize Date: Thu, 27 Jul 2023 17:11:27 +0200 Subject: [PATCH 5/6] =?UTF-8?q?[Updated]=C2=A0Other=20rewards=20responsive?= =?UTF-8?q?ness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Lists/OtherStakingRewards.tsx | 45 +++++++++---------- .../components/Lists/styles/Lists.scss | 8 ++++ src/utils/hooks.ts | 31 ++++++++++++- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/screens/Staking/components/Lists/OtherStakingRewards.tsx b/src/screens/Staking/components/Lists/OtherStakingRewards.tsx index fa008da7..f6c4be7b 100644 --- a/src/screens/Staking/components/Lists/OtherStakingRewards.tsx +++ b/src/screens/Staking/components/Lists/OtherStakingRewards.tsx @@ -9,19 +9,14 @@ import { CLIENT_PRECISION, LUM_ASSETS_GITHUB } from 'constant'; import { Card, ValidatorLogo } from 'frontend-elements'; import { Reward, Rewards } from 'models'; import { RootState } from 'redux/store'; -import { DenomsUtils, NumbersUtils, WalletClient, getExplorerLink, trunc } from 'utils'; +import { DenomsUtils, NumbersUtils, WalletClient, getExplorerLink, trunc, useWindowSize } from 'utils'; const OtherStakingRewards = ({ validators, otherRewards }: { validators: Validator[]; otherRewards: Rewards[] }) => { const prices = useSelector((state: RootState) => state.stats.prices); const { t } = useTranslation(); - const headers = [ - t('staking.tableLabels.validator'), - t('staking.tableLabels.token'), - t('staking.tableLabels.rewards'), - '', - ]; + const winSizes = useWindowSize(); const renderRow = (rewards: Reward, index: number) => { const normalDenom = DenomsUtils.computeDenom(rewards.reward[0].denom); @@ -30,7 +25,10 @@ const OtherStakingRewards = ({ validators, otherRewards }: { validators: Validat const validator = validators.find((val) => val.operatorAddress === rewards.validatorAddress); return ( -
+
-
-
-
- - - {DenomsUtils.computeDenom(rewards.reward[0].denom).toUpperCase()} - -
-
{price && numeral(amount * price.price).format('$0,0[.]00')}
+
+
+ + {DenomsUtils.computeDenom(rewards.reward[0].denom).toUpperCase()}
+
{price && numeral(amount * price.price).format('$0,0[.]00')}
); @@ -75,15 +69,18 @@ const OtherStakingRewards = ({ validators, otherRewards }: { validators: Validat 575.98} header={
- -
+ {winSizes.width > 575.98 && ( + + )} +
diff --git a/src/screens/Staking/components/Lists/styles/Lists.scss b/src/screens/Staking/components/Lists/styles/Lists.scss index 6821b182..035b7387 100644 --- a/src/screens/Staking/components/Lists/styles/Lists.scss +++ b/src/screens/Staking/components/Lists/styles/Lists.scss @@ -51,6 +51,10 @@ .other-rewards-card { box-shadow: none; background-color: $color-striped; + + .other-reward-collapse { + border-top: rgba(0, 0, 0, 0.2) 1px solid; + } } } @@ -115,6 +119,10 @@ .other-rewards-card { background-color: $color-dark-striped; + + .other-reward-collapse { + border-top: rgba(255, 255, 255, 0.2) 1px solid; + } } } } diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index c358535c..e4fc28dc 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; export const usePrevious = (value: T): T | undefined => { const ref = useRef(); @@ -7,3 +7,32 @@ export const usePrevious = (value: T): T | undefined => { }, [value]); return ref.current; }; + +interface WindowSize { + width: number; + height: number; +} + +export const useWindowSize = (): WindowSize => { + const [windowSize, setWindowSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + useEffect(() => { + // Handler to call on window resize + function handleResize() { + // Set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + // Add event listener + window.addEventListener('resize', handleResize); + // Call handler right away so state gets updated with initial window size + handleResize(); + // Remove event listener on cleanup + return () => window.removeEventListener('resize', handleResize); + }, []); + return windowSize; +}; From 21589d4bc8ee2a66e475e6e8f260615e82877b5d Mon Sep 17 00:00:00 2001 From: Karim Dalaize Date: Thu, 27 Jul 2023 17:16:02 +0200 Subject: [PATCH 6/6] [Updated] Fix tests --- src/tests/app.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/app.test.tsx b/src/tests/app.test.tsx index 2ac24c49..1f48f3ce 100644 --- a/src/tests/app.test.tsx +++ b/src/tests/app.test.tsx @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import React from 'react'; import { screen, within, render } from '@testing-library/react'; import i18n from 'locales';