diff --git a/.gitignore b/.gitignore index 7584be77..682ea7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ dist-ssr !.yarn/versions vite.config.ts.timestamp-* +coverage/ \ No newline at end of file diff --git a/package.json b/package.json index 2d641a48..84e55657 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "lint:ts": "tsc --noEmit", "test": "jest", "test:watch": "jest --watchAll=true", + "test:coverage": "jest --coverage", "codegen": "graphql-codegen --config codegen.ts", "format": "prettier . --write", "release": "semantic-release", @@ -89,6 +90,7 @@ "@testing-library/jest-dom": "^6.1.6", "@testing-library/preact": "^3.2.3", "@testing-library/preact-hooks": "^1.1.0", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", "@types/big.js": "^6.2.2", "@types/jest": "^29.5.11", diff --git a/src/app.tsx b/src/app.tsx index 7a68c85e..36e09a05 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -9,52 +9,81 @@ import { SuspenseLoad } from './components/Suspense'; import { config } from './config'; import TermsAndConditions from './TermsAndConditions'; +export enum PATHS { + DASHBOARD = 'dashboard', + GAS = 'gas', + SPACEWALK = 'spacewalk', + BRIDGE = 'bridge', + TRANSACTIONS = 'transactions', + NABLA = 'nabla', + NABLA_SWAP = 'swap', + NABLA_SWAP_POOLS = 'swap-pools', + NABLA_BACKSTOP_POOLS = 'backstop-pools', + STAKING = 'staking', +} + +export const PAGES_PATHS = { + DASHBOARD: PATHS.DASHBOARD, + GAS: PATHS.GAS, + BRIDGE: `${PATHS.SPACEWALK}/${PATHS.BRIDGE}`, + TRANSACTIONS: `${PATHS.SPACEWALK}/${PATHS.TRANSACTIONS}`, + NABLA: PATHS.NABLA, + NABLA_SWAP: `${PATHS.NABLA}/${PATHS.NABLA_SWAP}`, + NABLA_SWAP_POOLS: `${PATHS.NABLA}/${PATHS.NABLA_SWAP_POOLS}`, + NABLA_BACKSTOP_POOLS: `${PATHS.NABLA}/${PATHS.NABLA_BACKSTOP_POOLS}`, + STAKING: PATHS.STAKING, +}; + /** * Components need to be default exports inside the file for suspense loading to work properly */ -const Dashboard = import('./pages/dashboard/Dashboard')} fallback={defaultPageLoader} />; -const Gas = import('./pages/gas')} fallback={defaultPageLoader} />; -const NablaPage = import('./pages/nabla')} fallback={defaultPageLoader} />; -const StatsPage = import('./pages/stats')} fallback={defaultPageLoader} />; -const SwapPage = import('./pages/nabla/swap')} fallback={defaultPageLoader} />; -const SwapPoolsPage = import('./pages/nabla/swap-pools')} fallback={defaultPageLoader} />; -const TransactionsPage = ( - import('./pages/bridge/Transactions')} fallback={defaultPageLoader} /> -); -const BackstopPoolsPage = ( - import('./pages/nabla/backstop-pools')} fallback={defaultPageLoader} /> +const pages = import.meta.glob('./pages/**/index.tsx'); + +const loadPage = (path: string) => ( + ); -const Bridge = import('./pages/bridge')} fallback={defaultPageLoader} />; -const Staking = import('./pages/collators/Collators')} fallback={defaultPageLoader} />; -export const App = () => ( - <> - - } /> - }> - - - - - - - - - }> - - - - +const Dashboard = loadPage(PAGES_PATHS.DASHBOARD); +const Gas = loadPage(PAGES_PATHS.GAS); +const Bridge = loadPage(PAGES_PATHS.BRIDGE); +const TransactionsPage = loadPage(PAGES_PATHS.TRANSACTIONS); +const Staking = loadPage(PAGES_PATHS.STAKING); +const NablaPage = loadPage(PAGES_PATHS.NABLA); +const SwapPage = loadPage(PAGES_PATHS.NABLA_SWAP); +const SwapPoolsPage = loadPage(PAGES_PATHS.NABLA_SWAP_POOLS); +const BackstopPoolsPage = loadPage(PAGES_PATHS.NABLA_BACKSTOP_POOLS); + +export function App() { + return ( + <> + + } /> + }> + } /> + + + + + + + }> + + + + + } /> + + } /> } /> - - - - -
- {/* This is where the dialogs/modals are rendered. It is placed here because it is the highest point in the app where the tailwind data-theme is available */} -
- -); +
+ + +
+ {/* This is where the dialogs/modals are rendered. It is placed here because it is the highest point in the app where the tailwind data-theme is available */} +
+ + ); +} diff --git a/src/components/GetToken/index.tsx b/src/components/GetToken/index.tsx index f40f75c6..50c448e5 100644 --- a/src/components/GetToken/index.tsx +++ b/src/components/GetToken/index.tsx @@ -70,13 +70,13 @@ const getTokenIcon = (currentTenant: TenantName) => { }; export const GetToken = () => { - const { balance } = useAccountBalance(); + const { total } = useAccountBalance().balances; const { currentTenant } = useSwitchChain(); const { tokenSymbol } = useNodeInfoState().state; const link = `/${currentTenant}/gas`; - const isBalanceZero = Number(balance) === 0; + const isBalanceZero = Number(total) === 0; return (
diff --git a/src/components/Layout/Nav.tsx b/src/components/Layout/Nav.tsx index 6c06407c..a1ef319d 100644 --- a/src/components/Layout/Nav.tsx +++ b/src/components/Layout/Nav.tsx @@ -23,7 +23,7 @@ const CollapseMenu = ({ const isActive = useMemo(() => { const [path] = pathname.split('?'); const paths = path.split('/').filter(Boolean); - return paths[1].startsWith(link.replace('/', '')) ? true : false; + return paths[1] && paths[1].startsWith(link.replace('/', '')) ? true : false; }, [link, pathname]); const [isOpen, { toggle }] = useBoolean(isActive); diff --git a/src/components/Layout/links.tsx b/src/components/Layout/links.tsx index 45cf0180..50f6acdf 100644 --- a/src/components/Layout/links.tsx +++ b/src/components/Layout/links.tsx @@ -14,6 +14,7 @@ import { nablaConfig } from '../../config/apps/nabla'; import { GlobalState } from '../../GlobalStateProvider'; import { TenantName } from '../../models/Tenant'; import { getSpacewalkInterpolation, getSpacewalkText } from './spacewalkAnimation'; +import { PAGES_PATHS, PATHS } from '../../app'; export type LinkParameter = { isActive?: boolean }; @@ -55,7 +56,7 @@ export type Links = (state: Partial) => LinkItem[]; export const links: Links = ({ tenantName }) => [ { - link: './dashboard', + link: `./${PAGES_PATHS.DASHBOARD}`, title: 'Dashboard', props: { className: ({ isActive } = {}) => (isActive ? 'active' : ''), @@ -74,7 +75,7 @@ export const links: Links = ({ tenantName }) => [ suffix: , }, { - link: './spacewalk', + link: `/${PATHS.SPACEWALK}`, title: getSpacewalkText(tenantName), props: { className: ({ isActive } = {}) => (isActive ? 'active' : tenantName === TenantName.Pendulum ? 'active' : ''), @@ -82,17 +83,17 @@ export const links: Links = ({ tenantName }) => [ prefix: getSpacewalkInterpolation(tenantName), submenu: [ { - link: './spacewalk/bridge', + link: `./${PAGES_PATHS.BRIDGE}`, title: 'Bridge', }, { - link: './spacewalk/transactions', + link: `./${PAGES_PATHS.TRANSACTIONS}`, title: 'Transactions', }, ], }, { - link: '/nabla', + link: `/${PATHS.NABLA}`, title: 'Forex AMM', hidden: (nablaConfig.environment && !nablaConfig.environment.includes(config.env)) || @@ -103,21 +104,21 @@ export const links: Links = ({ tenantName }) => [ }, submenu: [ { - link: './nabla/swap', + link: `./${PAGES_PATHS.NABLA_SWAP}`, title: 'Swap', }, { - link: './nabla/swap-pools', + link: `./${PAGES_PATHS.NABLA_SWAP_POOLS}`, title: 'Swap Pools', }, { - link: './nabla/backstop-pools', + link: `./${PAGES_PATHS.NABLA_BACKSTOP_POOLS}`, title: 'Backstop Pool', }, ], }, { - link: './staking', + link: `./${PAGES_PATHS.STAKING}`, title: 'Staking', props: { className: ({ isActive } = {}) => (isActive ? 'active' : ''), diff --git a/src/components/Wallet/index.tsx b/src/components/Wallet/index.tsx index aa0180ee..e75a9859 100644 --- a/src/components/Wallet/index.tsx +++ b/src/components/Wallet/index.tsx @@ -19,8 +19,9 @@ interface Props { const OpenWallet = (props: Props): JSX.Element => { const { walletAccount, dAppName, setWalletAccount, removeWalletAccount } = useGlobalState(); const { wallet, address } = walletAccount || {}; - const { query, balance } = useAccountBalance(); + const { query, balances } = useAccountBalance(); const { ss58Format, tokenSymbol } = useNodeInfoState().state; + const { total: balance } = balances; return ( <> diff --git a/src/components/nabla/Pools/Backstop/BackstopPoolModals.tsx b/src/components/nabla/Pools/Backstop/BackstopPoolModals.tsx index f0f456de..2a0021c4 100644 --- a/src/components/nabla/Pools/Backstop/BackstopPoolModals.tsx +++ b/src/components/nabla/Pools/Backstop/BackstopPoolModals.tsx @@ -3,7 +3,7 @@ import { NablaInstanceBackstopPool } from '../../../../hooks/nabla/useNablaInsta import { ModalTypes, useModal } from '../../../../services/modal'; import AddLiquidity from './AddLiquidity'; import WithdrawLiquidity from './WithdrawLiquidity'; -import { Dialog } from '../../../../pages/collators/dialogs/Dialog'; +import { Dialog } from '../../../../pages/staking/dialogs/Dialog'; export type LiquidityModalProps = { data?: NablaInstanceBackstopPool; diff --git a/src/components/nabla/Pools/Swap/SwapPoolModals.tsx b/src/components/nabla/Pools/Swap/SwapPoolModals.tsx index 1bec25b7..e2c8c68e 100644 --- a/src/components/nabla/Pools/Swap/SwapPoolModals.tsx +++ b/src/components/nabla/Pools/Swap/SwapPoolModals.tsx @@ -1,6 +1,6 @@ import { FunctionalComponent } from 'preact'; import { ModalTypes, useModal } from '../../../../services/modal'; -import { Dialog } from '../../../../pages/collators/dialogs/Dialog'; +import { Dialog } from '../../../../pages/staking/dialogs/Dialog'; import { SwapPoolColumn } from './columns'; import AddLiquidity from './AddLiquidity'; import Redeem from './Redeem'; diff --git a/src/helpers/spacewalk.ts b/src/helpers/spacewalk.ts index a2bdc43d..d76f3b14 100644 --- a/src/helpers/spacewalk.ts +++ b/src/helpers/spacewalk.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { ApiPromise } from '@polkadot/api'; import { U8aFixed } from '@polkadot/types-codec'; import { H256 } from '@polkadot/types/interfaces'; @@ -65,7 +66,7 @@ export function currencyToStellarAssetCode(currency: SpacewalkPrimitivesCurrency export function convertStellarAssetToCurrency(asset: Asset, api: ApiPromise): SpacewalkPrimitivesCurrencyId { if (asset.isNative()) { - return api.createType('SpacewalkPrimitivesCurrencyId', 'StellarNative'); + return api.createType('SpacewalkPrimitivesCurrencyId', { Stellar: 'StellarNative' }); } else { const pair = Keypair.fromPublicKey(asset.getIssuer()); // We need the raw public key, not the base58 encoded version @@ -73,19 +74,25 @@ export function convertStellarAssetToCurrency(asset: Asset, api: ApiPromise): Sp const issuer = api.createType('Raw', issuerRawPublicKey, 32); if (asset.getCode().length <= 4) { - const code = api.createType('Raw', asset.getCode(), 4); + const paddedCode = _.padEnd(asset.getCode(), 4, '\0'); + const code = api.createType('Raw', paddedCode, 4); return api.createType('SpacewalkPrimitivesCurrencyId', { - AlphaNum4: { - code, - issuer, + Stellar: { + AlphaNum4: { + code, + issuer, + }, }, }); } else { - const code = api.createType('Raw', asset.getCode(), 12); + const paddedCode = _.padEnd(asset.getCode(), 12, '\0'); + const code = api.createType('Raw', paddedCode, 12); return api.createType('SpacewalkPrimitivesCurrencyId', { - AlphaNum12: { - code, - issuer, + Stellar: { + AlphaNum12: { + code, + issuer, + }, }, }); } diff --git a/src/hooks/spacewalk/__tests__/useCalculateGriefingCollateral.test.ts b/src/hooks/spacewalk/__tests__/useCalculateGriefingCollateral.test.ts new file mode 100644 index 00000000..e2b09447 --- /dev/null +++ b/src/hooks/spacewalk/__tests__/useCalculateGriefingCollateral.test.ts @@ -0,0 +1,95 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/preact'; +import { Asset } from 'stellar-base'; +import Big from 'big.js'; +import { useFeePallet } from '../useFeePallet'; +import { useCalculateGriefingCollateral } from '../useCalculateGriefingCollateral'; + +const STELLAR_ONE_WITH_DECIMALS = 1000000000000; +const VALID_ASSET = new Asset('code', 'GASOCNHNNLYFNMDJYQ3XFMI7BYHIOCFW3GJEOWRPEGK2TDPGTG2E5EDW'); +// We assume that the price is the same for all assets +const PRICE = 1; + +jest.mock('../../usePriceFetcher', () => ({ + usePriceFetcher: jest.fn().mockReturnValue({ + getTokenPriceForCurrency: jest.fn().mockReturnValue(Promise.resolve(PRICE)), + }), +})); + +jest.mock('../../../NodeInfoProvider', () => ({ + useNodeInfoState: jest.fn().mockReturnValue({ + state: { api: {} }, + }), +})); + +jest.mock('../../../helpers/spacewalk', () => ({ + convertStellarAssetToCurrency: jest.fn().mockImplementation(() => 'convertedCurrency'), +})); + +// useFeePallet mock is implemented in the describe() as we need to spy it during tests +jest.mock('../useFeePallet'); + +describe('useCalculateGriefingCollateral', () => { + const mockGetFees = jest.fn().mockReturnValue({ + griefingCollateralCurrency: 'AUDD', + issueGriefingCollateralFee: 0.05, + }); + + beforeEach(() => { + (useFeePallet as jest.MockedFunction).mockReturnValue({ + getFees: mockGetFees, + getTransactionFee: jest.fn(), + }); + + jest.useFakeTimers(); + }); + + it('should return 0 griefing collateral if amount is not valid', async () => { + const { result } = renderHook(() => useCalculateGriefingCollateral(new Big(0), VALID_ASSET)); + + const expectation = () => expect(result.current.eq(new Big(0))).toBeTruthy(); + + expectation(); + await waitFor(expectation); + }); + + it('should return 0 griefing collateral if bridgedAsset is not valid', async () => { + const { result } = renderHook(() => useCalculateGriefingCollateral(new Big(1000), undefined)); + + const expectation = () => expect(result.current.eq(new Big(0))).toBeTruthy(); + + expectation(); + await waitFor(expectation); + }); + + it('should return 0 griefing collateral if griefingCollateralCurrency is not valid', async () => { + mockGetFees.mockReturnValueOnce({ + griefingCollateralCurrency: undefined, + issueGriefingCollateralFee: 0.05, + }); + + const { result } = renderHook(() => useCalculateGriefingCollateral(new Big(1000), VALID_ASSET)); + + const expectation = () => expect(result.current.eq(new Big(0))).toBeTruthy(); + + expectation(); + await waitFor(expectation); + }); + + it('should calculate griefing collateral correctly', async () => { + const amount = new Big(STELLAR_ONE_WITH_DECIMALS); + const { result } = renderHook(() => useCalculateGriefingCollateral(amount, VALID_ASSET)); + + // First returned value (before useEffect is fired) + expect(result.current.eq(0)).toBeTruthy(); + + const griefingFee = mockGetFees().issueGriefingCollateralFee; + // Since we assume the same price for all assets, the decimal griefing collateral is the amount * griefingFee / decimals + const griefingAmount = amount + .times(griefingFee) + .div(10 ** 12) + .toNumber(); + + await waitFor(() => expect(result.current.eq(griefingAmount)).toBeTruthy()); + }); +}); diff --git a/src/hooks/spacewalk/useBridgeSettings.ts b/src/hooks/spacewalk/useBridgeSettings.ts index e30861d9..bcf18f43 100644 --- a/src/hooks/spacewalk/useBridgeSettings.ts +++ b/src/hooks/spacewalk/useBridgeSettings.ts @@ -6,7 +6,7 @@ import { Asset } from 'stellar-sdk'; import { useGlobalState } from '../../GlobalStateProvider'; import { convertCurrencyToStellarAsset, shouldFilterOut } from '../../helpers/spacewalk'; import { stringifyStellarAsset } from '../../helpers/stellar'; -import { BridgeContext } from '../../pages/bridge'; +import { BridgeContext } from '../../pages/spacewalk/bridge'; import { ExtendedRegistryVault, useVaultRegistryPallet } from './useVaultRegistryPallet'; import { ToastMessage, showToast } from '../../shared/showToast'; import { Balance } from '@polkadot/types/interfaces'; diff --git a/src/hooks/spacewalk/useCalculateGriefingCollateral.ts b/src/hooks/spacewalk/useCalculateGriefingCollateral.ts new file mode 100644 index 00000000..96fd2359 --- /dev/null +++ b/src/hooks/spacewalk/useCalculateGriefingCollateral.ts @@ -0,0 +1,49 @@ +import { Asset } from 'stellar-base'; +import Big from 'big.js'; +import { useEffect, useState, useMemo } from 'preact/compat'; +import { nativeStellarToDecimal } from '../../shared/parseNumbers/metric'; +import { convertStellarAssetToCurrency } from '../../helpers/spacewalk'; +import { useNodeInfoState } from '../../NodeInfoProvider'; +import { usePriceFetcher } from '../usePriceFetcher'; +import { useFeePallet } from './useFeePallet'; + +const isInvalid = (value: unknown) => { + return !value; +}; + +export const useCalculateGriefingCollateral = (amount: Big, bridgedAsset?: Asset): Big.Big => { + const { getTokenPriceForCurrency } = usePriceFetcher(); + const { getFees } = useFeePallet(); + const [griefingCollateral, setGriefingCollateral] = useState(new Big(0)); + const { api } = useNodeInfoState().state; + + const { griefingCollateralCurrency, issueGriefingCollateralFee } = getFees(); + + const bridgedCurrency = useMemo(() => { + return bridgedAsset && api ? convertStellarAssetToCurrency(bridgedAsset, api) : null; + }, [bridgedAsset, api]); + + useEffect(() => { + const calculateGriefingCollateral = async () => { + if (isInvalid(amount) || isInvalid(bridgedCurrency) || isInvalid(griefingCollateralCurrency)) return; + + if (bridgedCurrency && griefingCollateralCurrency) { + try { + const assetUSDPrice = await getTokenPriceForCurrency(bridgedCurrency); + const amountUSD = nativeStellarToDecimal(amount).mul(assetUSDPrice); + + const griefingCollateralCurrencyUSD = await getTokenPriceForCurrency(griefingCollateralCurrency); + if (isInvalid(griefingCollateralCurrencyUSD)) return; + + setGriefingCollateral(amountUSD.mul(issueGriefingCollateralFee).div(griefingCollateralCurrencyUSD)); + } catch (error) { + console.error('Error calculating griefing collateral:', error); + } + } + }; + + calculateGriefingCollateral(); + }, [amount, getTokenPriceForCurrency, griefingCollateralCurrency, issueGriefingCollateralFee, bridgedCurrency]); + + return griefingCollateral; +}; diff --git a/src/hooks/spacewalk/useFeePallet.tsx b/src/hooks/spacewalk/useFeePallet.tsx index 43b45255..0b4a6b21 100644 --- a/src/hooks/spacewalk/useFeePallet.tsx +++ b/src/hooks/spacewalk/useFeePallet.tsx @@ -10,8 +10,8 @@ export function useFeePallet() { const [redeemFee, setRedeemFee] = useState(new Big(0)); const [punishmentFee, setPunishmentFee] = useState(new Big(0)); const [premiumRedeemFee, setPremiumRedeemFee] = useState(new Big(0)); - const [issueGriefingCollateral, setIssueGriefingCollateral] = useState(new Big(0)); - const [replaceGriefingCollateral, setReplaceGriefingCollateral] = useState(new Big(0)); + const [issueGriefingCollateralFee, setIssueGriefingCollateralFee] = useState(new Big(0)); + const [replaceGriefingCollateralFee, setReplaceGriefingCollateralFee] = useState(new Big(0)); const [griefingCollateralCurrency, setGriefingCollateralCurrency] = useState< SpacewalkPrimitivesCurrencyId | undefined @@ -50,17 +50,17 @@ export function useFeePallet() { const decimal = Big(fixedPointToDecimal(fee.toString())); setPremiumRedeemFee(decimal); }), - api.query.fee.issueGriefingCollateral((fee) => { + api.query.fee.issueGriefingCollateral((fee: Big) => { const decimal = Big(fixedPointToDecimal(fee.toString())); - setIssueGriefingCollateral(decimal); + setIssueGriefingCollateralFee(decimal); }), - api.query.fee.replaceGriefingCollateral((fee) => { + api.query.fee.replaceGriefingCollateral((fee: Big) => { const decimal = Big(fixedPointToDecimal(fee.toString())); - setReplaceGriefingCollateral(decimal); + setReplaceGriefingCollateralFee(decimal); }), ]).then((unsubscribeFunctions) => { unsubscribe = () => { - unsubscribeFunctions.forEach((u) => u()); + unsubscribeFunctions.forEach((u) => (u as () => void)()); }; }); @@ -75,8 +75,8 @@ export function useFeePallet() { redeemFee, punishmentFee, premiumRedeemFee, - issueGriefingCollateral, - replaceGriefingCollateral, + issueGriefingCollateralFee, + replaceGriefingCollateralFee, griefingCollateralCurrency, }; }, @@ -99,8 +99,8 @@ export function useFeePallet() { redeemFee, punishmentFee, premiumRedeemFee, - issueGriefingCollateral, - replaceGriefingCollateral, + issueGriefingCollateralFee, + replaceGriefingCollateralFee, griefingCollateralCurrency, ]); diff --git a/src/hooks/useAssetRegistryMetadata.ts b/src/hooks/useAssetRegistryMetadata.ts index 877fa0e8..52a74ad5 100644 --- a/src/hooks/useAssetRegistryMetadata.ts +++ b/src/hooks/useAssetRegistryMetadata.ts @@ -29,9 +29,11 @@ interface UseAssetRegistryMetadata { function convertToOrmlAssetRegistryAssetMetadata(metadata: [StorageKey, Codec]): OrmlTraitsAssetRegistryAssetMetadata { const { decimals, name, symbol, additional } = metadata[1].toHuman() as CurrencyMetadataType; - const currencyIdArray = metadata[0].toHuman() as unknown as SpacewalkPrimitivesCurrencyId[]; - const currencyId = currencyIdArray[0]; - + const currencyIdStorageKey = metadata[0] as StorageKey; + // We need to convert the currencyId StorageKey to the correct type + const currencyIdJsonArray = currencyIdStorageKey.toHuman() as unknown as SpacewalkPrimitivesCurrencyId[]; + const currencyIdJson = currencyIdJsonArray[0]; + const currencyId = currencyIdStorageKey.registry.createType('SpacewalkPrimitivesCurrencyId', currencyIdJson); return { metadata: { decimals: Number(decimals), name, symbol, additional }, currencyId, diff --git a/src/hooks/useBalances.ts b/src/hooks/useBalances.ts index 95bd12ee..46755164 100644 --- a/src/hooks/useBalances.ts +++ b/src/hooks/useBalances.ts @@ -25,7 +25,7 @@ function useBalances() { const fetchTokenBalance = useCallback( async (address: string, currencyId: SpacewalkPrimitivesCurrencyId) => { if (!api) return; - const isNativeToken = typeof currencyId === 'string' && currencyId === 'Native'; + const isNativeToken = currencyId.toHuman() === 'Native'; if (isNativeToken) { return api.query.system.account(address); } @@ -42,7 +42,7 @@ function useBalances() { const walletAddress = ss58Format ? getAddressForFormat(walletAccount.address, ss58Format) : walletAccount.address; const getFree = (tokenBalanceRaw: unknown, asset: OrmlTraitsAssetRegistryAssetMetadata) => { - const isNativeToken = typeof asset.currencyId === 'string' && asset.currencyId === 'Native'; + const isNativeToken = asset.currencyId.toHuman() === 'Native'; if (isNativeToken) { return (tokenBalanceRaw as { data: { free: Big } }).data.free; } diff --git a/src/hooks/useBuyout/index.ts b/src/hooks/useBuyout/index.ts index 697129b0..0bb4869d 100644 --- a/src/hooks/useBuyout/index.ts +++ b/src/hooks/useBuyout/index.ts @@ -6,7 +6,7 @@ import { Codec } from '@polkadot/types-codec/types'; import { useNodeInfoState } from '../../NodeInfoProvider'; import { nativeToFormatDecimalPure } from '../../shared/parseNumbers/decimal'; -import { doSubmitExtrinsic } from '../../pages/collators/dialogs/helpers'; +import { doSubmitExtrinsic } from '../../pages/staking/dialogs/helpers'; import { useGlobalState } from '../../GlobalStateProvider'; import { OrmlTraitsAssetRegistryAssetMetadata } from './types'; diff --git a/src/hooks/usePriceFetcher.ts b/src/hooks/usePriceFetcher.ts index 4ba82257..aa460cdb 100644 --- a/src/hooks/usePriceFetcher.ts +++ b/src/hooks/usePriceFetcher.ts @@ -1,4 +1,6 @@ import { useCallback, useEffect, useState } from 'preact/compat'; +import { TenantName } from '../models/Tenant'; +import useSwitchChain from './useSwitchChain'; import { useNodeInfoState } from '../NodeInfoProvider'; import { nativeToDecimal } from '../shared/parseNumbers/metric'; import { SpacewalkPrimitivesCurrencyId } from '@polkadot/types/lookup'; @@ -19,6 +21,7 @@ type PricesCache = { [diaKeys: string]: number }; export const usePriceFetcher = () => { const [pricesCache, setPricesCache] = useState({}); + const { currentTenant } = useSwitchChain(); const { api } = useNodeInfoState().state; const { getAllAssetsMetadata } = useAssetRegistryMetadata(); @@ -64,15 +67,23 @@ export const usePriceFetcher = () => { [pricesCache], ); + const handleNativeTokenPrice = useCallback(() => { + if (currentTenant === TenantName.Pendulum) return pricesCache['Pendulum:PEN']; + return pricesCache['Amplitude:AMPE']; + }, [currentTenant, pricesCache]); + const getTokenPriceForCurrency = useCallback( async (currency: SpacewalkPrimitivesCurrencyId) => { - const asset = getAllAssetsMetadata().find((asset) => isEqual(asset.currencyId, currency)); + if (currency.toHuman() === 'Native') return handleNativeTokenPrice(); + const asset = getAllAssetsMetadata().find((asset) => { + return isEqual(asset.currencyId.toHuman(), currency.toHuman()); + }); if (!asset) { return 0; } return getTokenPriceForKeys(asset.metadata.additional.diaKeys); }, - [getAllAssetsMetadata, getTokenPriceForKeys], + [getAllAssetsMetadata, getTokenPriceForKeys, handleNativeTokenPrice], ); return { getTokenPriceForKeys, getTokenPriceForCurrency }; diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/index.tsx similarity index 100% rename from src/pages/dashboard/Dashboard.tsx rename to src/pages/dashboard/index.tsx diff --git a/src/pages/gas/GasSuccessDialog.tsx b/src/pages/gas/GasSuccessDialog.tsx index 8d9323d0..88215484 100644 --- a/src/pages/gas/GasSuccessDialog.tsx +++ b/src/pages/gas/GasSuccessDialog.tsx @@ -1,6 +1,6 @@ import { Button } from 'react-daisyui'; import SuccessDialogIcon from '../../assets/dialog-status-success'; -import { Dialog } from '../collators/dialogs/Dialog'; +import { Dialog } from '../staking/dialogs/Dialog'; interface DialogProps { visible: boolean; diff --git a/src/pages/bridge/FeeBox.tsx b/src/pages/spacewalk/bridge/FeeBox.tsx similarity index 84% rename from src/pages/bridge/FeeBox.tsx rename to src/pages/spacewalk/bridge/FeeBox.tsx index 108cf5ec..310e70fb 100644 --- a/src/pages/bridge/FeeBox.tsx +++ b/src/pages/spacewalk/bridge/FeeBox.tsx @@ -2,8 +2,8 @@ import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; import Big from 'big.js'; import { useCallback, useEffect, useMemo, useState } from 'preact/compat'; import { Asset } from 'stellar-sdk'; -import { useFeePallet } from '../../hooks/spacewalk/useFeePallet'; -import { nativeStellarToDecimal, nativeToDecimal } from '../../shared/parseNumbers/metric'; +import { useFeePallet } from '../../../hooks/spacewalk/useFeePallet'; +import { ChainDecimals, nativeStellarToDecimal, nativeToDecimal } from '../../../shared/parseNumbers/metric'; interface FeeBoxProps { // The amount of the bridged asset denoted in the smallest unit of the asset @@ -12,12 +12,12 @@ interface FeeBoxProps { extrinsic?: SubmittableExtrinsic; nativeCurrency: string; network: string; - showSecurityDeposit?: boolean; + securityDeposit?: Big; wrappedCurrencySuffix?: string; } export function FeeBox(props: FeeBoxProps): JSX.Element { - const { bridgedAsset, extrinsic, network, wrappedCurrencySuffix, nativeCurrency, showSecurityDeposit } = props; + const { bridgedAsset, extrinsic, network, wrappedCurrencySuffix, nativeCurrency, securityDeposit } = props; const amount = props.amountNative; const wrappedCurrencyName = bridgedAsset ? bridgedAsset.getCode() + (wrappedCurrencySuffix || '') : ''; const { getFees, getTransactionFee } = useFeePallet(); @@ -40,10 +40,6 @@ export function FeeBox(props: FeeBoxProps): JSX.Element { return nativeStellarToDecimal(amount.mul(fees.issueFee)); }, [amount, fees]); - const griefingCollateral = useMemo(() => { - return nativeStellarToDecimal(amount.mul(fees.issueGriefingCollateral)); - }, [amount, fees]); - const toggle = useCallback(() => { if (collapseVisibility === '') { setCollapseVisibility('collapse-open'); @@ -63,6 +59,7 @@ export function FeeBox(props: FeeBoxProps): JSX.Element { return nativeStellarToDecimal(amount).sub(bridgeFee); }, [amount, bridgeFee]); + return (
- {showSecurityDeposit && ( + {securityDeposit && (
Security Deposit - {griefingCollateral.toString()} {nativeCurrency} + {securityDeposit.toFixed(ChainDecimals)} {nativeCurrency}
)}
Transaction Fee - {transactionFee.toFixed(12)} {nativeCurrency} + {transactionFee.toFixed(ChainDecimals)} {nativeCurrency}
diff --git a/src/pages/bridge/Issue/ConfirmationDialog.tsx b/src/pages/spacewalk/bridge/Issue/ConfirmationDialog.tsx similarity index 84% rename from src/pages/bridge/Issue/ConfirmationDialog.tsx rename to src/pages/spacewalk/bridge/Issue/ConfirmationDialog.tsx index c243e1da..be9304c3 100644 --- a/src/pages/bridge/Issue/ConfirmationDialog.tsx +++ b/src/pages/spacewalk/bridge/Issue/ConfirmationDialog.tsx @@ -1,15 +1,16 @@ import { useMemo } from 'preact/compat'; import { Button, Divider } from 'react-daisyui'; -import { CopyableAddress, PublicKey } from '../../../components/PublicKey'; -import TransferCountdown from '../../../components/TransferCountdown'; -import { convertCurrencyToStellarAsset, deriveShortenedRequestId } from '../../../helpers/spacewalk'; -import { convertRawHexKeyToPublicKey } from '../../../helpers/stellar'; -import { RichIssueRequest } from '../../../hooks/spacewalk/useIssuePallet'; -import { nativeStellarToDecimal } from '../../../shared/parseNumbers/metric'; -import { Dialog } from '../../collators/dialogs/Dialog'; -import { generateSEP0007URIScheme } from '../../../helpers/stellar/sep0007'; +import { CopyableAddress, PublicKey } from '../../../../components/PublicKey'; +import TransferCountdown from '../../../../components/TransferCountdown'; +import { convertCurrencyToStellarAsset, deriveShortenedRequestId } from '../../../../helpers/spacewalk'; +import { convertRawHexKeyToPublicKey } from '../../../../helpers/stellar'; +import { RichIssueRequest } from '../../../../hooks/spacewalk/useIssuePallet'; +import { nativeStellarToDecimal } from '../../../../shared/parseNumbers/metric'; +import { Dialog } from '../../../staking/dialogs/Dialog'; +import { generateSEP0007URIScheme } from '../../../../helpers/stellar/sep0007'; import { StellarUriScheme } from './StellarURIScheme'; +import { PENDULUM_SUPPORT_CHAT_URL } from '../../../../shared/constants'; interface ConfirmationDialogProps { issueRequest: RichIssueRequest | undefined; @@ -100,7 +101,7 @@ export function ConfirmationDialog(props: ConfirmationDialogProps): JSX.Element
  • Estimated time for issuing is in a minute after submitting the Stellar payment to the vault, contact - + support if your transaction is still pending after 10 minutes. diff --git a/src/pages/bridge/Issue/Disclaimer.tsx b/src/pages/spacewalk/bridge/Issue/Disclaimer.tsx similarity index 95% rename from src/pages/bridge/Issue/Disclaimer.tsx rename to src/pages/spacewalk/bridge/Issue/Disclaimer.tsx index 86fe6790..c356071b 100644 --- a/src/pages/bridge/Issue/Disclaimer.tsx +++ b/src/pages/spacewalk/bridge/Issue/Disclaimer.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'preact/compat'; -import BellIcon from '../../../assets/bell'; +import BellIcon from '../../../../assets/bell'; type Props = { content: JSX.Element; diff --git a/src/pages/bridge/Issue/IssueValidationSchema.ts b/src/pages/spacewalk/bridge/Issue/IssueValidationSchema.ts similarity index 91% rename from src/pages/bridge/Issue/IssueValidationSchema.ts rename to src/pages/spacewalk/bridge/Issue/IssueValidationSchema.ts index 8dbeab40..1c050395 100644 --- a/src/pages/bridge/Issue/IssueValidationSchema.ts +++ b/src/pages/spacewalk/bridge/Issue/IssueValidationSchema.ts @@ -1,6 +1,6 @@ import * as Yup from 'yup'; import { IssueFormValues } from '.'; -import { transformNumber } from '../../../helpers/yup'; +import { transformNumber } from '../../../../helpers/yup'; export function getIssueValidationSchema(maxIssuable: number, balance: number, tokenSymbol?: string) { return Yup.object().shape({ diff --git a/src/pages/bridge/Issue/SettingsDialog.tsx b/src/pages/spacewalk/bridge/Issue/SettingsDialog.tsx similarity index 89% rename from src/pages/bridge/Issue/SettingsDialog.tsx rename to src/pages/spacewalk/bridge/Issue/SettingsDialog.tsx index de914213..e14fb962 100644 --- a/src/pages/bridge/Issue/SettingsDialog.tsx +++ b/src/pages/spacewalk/bridge/Issue/SettingsDialog.tsx @@ -1,7 +1,7 @@ import { Button, Checkbox } from 'react-daisyui'; -import VaultSelector from '../../../components/Selector/VaultSelector'; -import useBridgeSettings from '../../../hooks/spacewalk/useBridgeSettings'; -import { Dialog } from '../../collators/dialogs/Dialog'; +import VaultSelector from '../../../../components/Selector/VaultSelector'; +import useBridgeSettings from '../../../../hooks/spacewalk/useBridgeSettings'; +import { Dialog } from '../../../staking/dialogs/Dialog'; import { useMemo } from 'preact/hooks'; interface Props { diff --git a/src/pages/bridge/Issue/StellarURIScheme.tsx b/src/pages/spacewalk/bridge/Issue/StellarURIScheme.tsx similarity index 100% rename from src/pages/bridge/Issue/StellarURIScheme.tsx rename to src/pages/spacewalk/bridge/Issue/StellarURIScheme.tsx diff --git a/src/pages/bridge/Issue/index.tsx b/src/pages/spacewalk/bridge/Issue/index.tsx similarity index 83% rename from src/pages/bridge/Issue/index.tsx rename to src/pages/spacewalk/bridge/Issue/index.tsx index efb941bc..0c368830 100644 --- a/src/pages/bridge/Issue/index.tsx +++ b/src/pages/spacewalk/bridge/Issue/index.tsx @@ -7,18 +7,18 @@ import { Button } from 'react-daisyui'; import { FieldErrors, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; -import { useGlobalState } from '../../../GlobalStateProvider'; -import { useNodeInfoState } from '../../../NodeInfoProvider'; -import From from '../../../components/Form/From'; -import OpenWallet from '../../../components/Wallet'; -import { getErrors, getEventBySectionAndMethod } from '../../../helpers/substrate'; -import { useFeePallet } from '../../../hooks/spacewalk/useFeePallet'; -import { RichIssueRequest, useIssuePallet } from '../../../hooks/spacewalk/useIssuePallet'; -import useBridgeSettings from '../../../hooks/spacewalk/useBridgeSettings'; -import { decimalToStellarNative, nativeToDecimal } from '../../../shared/parseNumbers/metric'; -import { useAccountBalance } from '../../../shared/useAccountBalance'; -import { TenantName } from '../../../models/Tenant'; -import { ToastMessage, showToast } from '../../../shared/showToast'; +import { useGlobalState } from '../../../../GlobalStateProvider'; +import { useNodeInfoState } from '../../../../NodeInfoProvider'; +import From from '../../../../components/Form/From'; +import OpenWallet from '../../../../components/Wallet'; +import { getErrors, getEventBySectionAndMethod } from '../../../../helpers/substrate'; +import { RichIssueRequest, useIssuePallet } from '../../../../hooks/spacewalk/useIssuePallet'; +import useBridgeSettings from '../../../../hooks/spacewalk/useBridgeSettings'; +import { useCalculateGriefingCollateral } from '../../../../hooks/spacewalk/useCalculateGriefingCollateral'; +import { decimalToStellarNative, nativeToDecimal } from '../../../../shared/parseNumbers/metric'; +import { useAccountBalance } from '../../../../shared/useAccountBalance'; +import { TenantName } from '../../../../models/Tenant'; +import { ToastMessage, showToast } from '../../../../shared/showToast'; import { FeeBox } from '../FeeBox'; import { prioritizeXLMAsset } from '../helpers'; @@ -26,8 +26,10 @@ import { prioritizeXLMAsset } from '../helpers'; import { ConfirmationDialog } from './ConfirmationDialog'; import Disclaimer from './Disclaimer'; import { getIssueValidationSchema } from './IssueValidationSchema'; -import { isU128Compatible } from '../../../shared/parseNumbers/isU128Compatible'; -import { USER_INPUT_MAX_DECIMALS } from '../../../shared/parseNumbers/decimal'; +import { isU128Compatible } from '../../../../shared/parseNumbers/isU128Compatible'; +import { USER_INPUT_MAX_DECIMALS } from '../../../../shared/parseNumbers/decimal'; +import { PENDULUM_SUPPORT_CHAT_URL } from '../../../../shared/constants'; +import { PAGES_PATHS } from '../../../../app'; interface IssueProps { network: string; @@ -67,15 +69,15 @@ function Issue(props: IssueProps): JSX.Element { const { walletAccount, tenantName } = useGlobalState(); const { api, tokenSymbol } = useNodeInfoState().state; const { selectedVault, selectedAsset, setSelectedAsset, wrappedAssets } = useBridgeSettings(); - const { issueGriefingCollateral } = useFeePallet().getFees(); - const { balance } = useAccountBalance(); + const { balances } = useAccountBalance(); + const { transferable } = balances; const issuableTokens = selectedVault?.issuableTokens?.toJSON?.().amount ?? selectedVault?.issuableTokens; const maxIssuable = nativeToDecimal(issuableTokens || 0).toNumber(); const { handleSubmit, watch, register, formState, setValue, trigger } = useForm({ - resolver: yupResolver(getIssueValidationSchema(maxIssuable, parseFloat(balance || '0.0'), tokenSymbol)), + resolver: yupResolver(getIssueValidationSchema(maxIssuable, parseFloat(transferable || '0.0'), tokenSymbol)), mode: 'onChange', }); @@ -87,6 +89,8 @@ function Issue(props: IssueProps): JSX.Element { return amount ? decimalToStellarNative(amount) : Big(0); }, [amount]); + const griefingCollateral = useCalculateGriefingCollateral(amountNative, selectedAsset); + const disclaimerContent = useMemo( () => (
      @@ -107,7 +111,7 @@ function Issue(props: IssueProps): JSX.Element {
    • Estimated time for issuing: In a minute after submitting the Stellar payment to the vault. Contact - + support if your transaction is still pending after 10 minutes. @@ -180,9 +184,9 @@ function Issue(props: IssueProps): JSX.Element { ); useEffect(() => { - setValue('securityDeposit', amount * issueGriefingCollateral.toNumber()); + setValue('securityDeposit', griefingCollateral.toNumber()); trigger('securityDeposit'); - }, [amount, issueGriefingCollateral, setValue, trigger]); + }, [amount, griefingCollateral, setValue, trigger]); useEffect(() => { // Trigger form validation when the selected asset changes @@ -197,7 +201,7 @@ function Issue(props: IssueProps): JSX.Element { onClose={() => setConfirmationDialogVisible(false)} onConfirm={() => { setConfirmationDialogVisible(false); - navigateTo(`/${tenantName}/spacewalk/transactions`); + navigateTo(`/${tenantName}${PAGES_PATHS.TRANSACTIONS}`); }} />
      @@ -234,7 +238,7 @@ function Issue(props: IssueProps): JSX.Element { extrinsic={requestIssueExtrinsic} nativeCurrency={nativeCurrency} network={network} - showSecurityDeposit + securityDeposit={griefingCollateral} wrappedCurrencySuffix={wrappedCurrencySuffix} /> {walletAccount ? ( diff --git a/src/pages/bridge/Redeem/ConfirmationDialog.tsx b/src/pages/spacewalk/bridge/Redeem/ConfirmationDialog.tsx similarity index 71% rename from src/pages/bridge/Redeem/ConfirmationDialog.tsx rename to src/pages/spacewalk/bridge/Redeem/ConfirmationDialog.tsx index d7f671a6..80ec7f86 100644 --- a/src/pages/bridge/Redeem/ConfirmationDialog.tsx +++ b/src/pages/spacewalk/bridge/Redeem/ConfirmationDialog.tsx @@ -1,12 +1,14 @@ import { Button } from 'react-daisyui'; import { useNavigate } from 'react-router-dom'; -import { useGlobalState } from '../../../GlobalStateProvider'; -import { PublicKey } from '../../../components/PublicKey'; -import { convertCurrencyToStellarAsset } from '../../../helpers/spacewalk'; -import { RichRedeemRequest } from '../../../hooks/spacewalk/useRedeemPallet'; -import { nativeStellarToDecimal } from '../../../shared/parseNumbers/metric'; -import { Dialog } from '../../collators/dialogs/Dialog'; +import { useGlobalState } from '../../../../GlobalStateProvider'; +import { PublicKey } from '../../../../components/PublicKey'; +import { convertCurrencyToStellarAsset } from '../../../../helpers/spacewalk'; +import { RichRedeemRequest } from '../../../../hooks/spacewalk/useRedeemPallet'; +import { nativeStellarToDecimal } from '../../../../shared/parseNumbers/metric'; +import { Dialog } from '../../../staking/dialogs/Dialog'; import { useMemo } from 'preact/hooks'; +import { PENDULUM_SUPPORT_CHAT_URL } from '../../../../shared/constants'; +import { PAGES_PATHS } from '../../../../app'; interface ConfirmationDialogProps { redeemRequest: RichRedeemRequest | undefined; @@ -40,7 +42,7 @@ export function ConfirmationDialog(props: ConfirmationDialogProps): JSX.Element
      This typically takes only a few minutes. Contact - + support if your transaction is still pending after 10 minutes. @@ -56,7 +58,7 @@ export function ConfirmationDialog(props: ConfirmationDialogProps): JSX.Element