diff --git a/packages/site/src/common/config/config.base.json b/packages/site/src/common/config/config.base.json index 2c141138..fbae9e34 100644 --- a/packages/site/src/common/config/config.base.json +++ b/packages/site/src/common/config/config.base.json @@ -21,8 +21,18 @@ "mockedAddress": "rXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "fiatDecimals": 2, "xrplNetwork": { - "baseReserveCostInXrp": "10", - "ownerReserveCostInXrpPerItem": "2" + "mainnet": { + "baseReserveCostInXrp": "1", + "ownerReserveCostInXrpPerItem": "0.2" + }, + "testnet": { + "baseReserveCostInXrp": "10", + "ownerReserveCostInXrpPerItem": "2" + }, + "devnet": { + "baseReserveCostInXrp": "10", + "ownerReserveCostInXrpPerItem": "2" + } }, "featureFlags": { "enablePlayground": false diff --git a/packages/site/src/common/config/config.types.ts b/packages/site/src/common/config/config.types.ts index 4831ef20..5da4838d 100644 --- a/packages/site/src/common/config/config.types.ts +++ b/packages/site/src/common/config/config.types.ts @@ -26,7 +26,7 @@ export type Config = { }; mockedAddress: string; fiatDecimals: number; - xrplNetwork: NetworkReserve; + xrplNetwork: Record<'mainnet' | 'testnet' | 'devnet', NetworkReserve>; featureFlags: { enablePlayground: boolean; }; diff --git a/packages/site/src/data-access/repository/xrpl/XrplService.ts b/packages/site/src/data-access/repository/xrpl/XrplService.ts index d30047c3..a35808d1 100644 --- a/packages/site/src/data-access/repository/xrpl/XrplService.ts +++ b/packages/site/src/data-access/repository/xrpl/XrplService.ts @@ -87,6 +87,20 @@ export class XrplService { }); } + public async getNetworkReserve(): Promise<{ baseReserve?: number; ownerReserve?: number }> { + try { + const client = await this.getClient(); + const serverInfo = await client.request({ command: 'server_info' }); + + return { + baseReserve: serverInfo.result.info.validated_ledger?.reserve_base_xrp, + ownerReserve: serverInfo.result.info.validated_ledger?.reserve_inc_xrp, + }; + } catch (e) { + return {}; + } + } + public async getAccountInfo(account: string): Promise { try { const client = await this.getClient(); diff --git a/packages/site/src/domain/network/controller/NetworkController.ts b/packages/site/src/domain/network/controller/NetworkController.ts index 29e5c021..6f730c89 100644 --- a/packages/site/src/domain/network/controller/NetworkController.ts +++ b/packages/site/src/domain/network/controller/NetworkController.ts @@ -8,6 +8,10 @@ import type { MetaMaskRepository } from '../../../data-access/repository/metamas import { XrplService } from '../../../data-access/repository/xrpl/XrplService'; export default class NetworkController { + private baseReserveCostInXrp: string; + + private ownerReserveCostInXrpPerItem: string; + constructor(private readonly metamaskRepository: MetaMaskRepository, private readonly xrplService: XrplService) {} async load(): Promise { @@ -16,22 +20,32 @@ export default class NetworkController { switch (chainId) { case NetworkChainId.DEVNET: node = config.nodeUrls.devnet; + this.baseReserveCostInXrp = config.xrplNetwork.devnet.baseReserveCostInXrp; + this.ownerReserveCostInXrpPerItem = config.xrplNetwork.devnet.ownerReserveCostInXrpPerItem; break; case NetworkChainId.TESTNET: node = config.nodeUrls.testnet; + this.baseReserveCostInXrp = config.xrplNetwork.testnet.baseReserveCostInXrp; + this.ownerReserveCostInXrpPerItem = config.xrplNetwork.testnet.ownerReserveCostInXrpPerItem; break; default: node = config.nodeUrls.mainnet; + this.baseReserveCostInXrp = config.xrplNetwork.mainnet.baseReserveCostInXrp; + this.ownerReserveCostInXrpPerItem = config.xrplNetwork.mainnet.ownerReserveCostInXrpPerItem; break; } await withRetries(async () => this.xrplService.load(node), config.retry.times, config.retry.delay); + + const { baseReserve, ownerReserve } = await this.xrplService.getNetworkReserve(); + this.baseReserveCostInXrp = String(baseReserve || this.baseReserveCostInXrp); + this.ownerReserveCostInXrpPerItem = String(ownerReserve || this.ownerReserveCostInXrpPerItem); } getNetworkReserve(): NetworkReserve { return { - baseReserveCostInXrp: config.xrplNetwork.baseReserveCostInXrp, - ownerReserveCostInXrpPerItem: config.xrplNetwork.ownerReserveCostInXrpPerItem, + baseReserveCostInXrp: this.baseReserveCostInXrp, + ownerReserveCostInXrpPerItem: this.ownerReserveCostInXrpPerItem, } as const; } diff --git a/packages/site/src/ui/locale/locales/en/common.json b/packages/site/src/ui/locale/locales/en/common.json index d913d3f4..29a7209a 100644 --- a/packages/site/src/ui/locale/locales/en/common.json +++ b/packages/site/src/ui/locale/locales/en/common.json @@ -61,7 +61,7 @@ "expendable": "Expendable", "reserve": "Reserve", "balanceInfoExplanationTitle": "Why a non-expendable reserve?", - "balanceInfoExplanation": "XRPL accounts require a minimum reserve of 10 XRP to prevent spam and cover the computational costs of maintaining the network, ensuring account integrity and discouraging frivolous account creation. For each OwnerCount, an additional 2 XRP is required.", + "balanceInfoExplanation": "XRPL accounts require a minimum reserve of {{baseReserveCostInXrp}} XRP to prevent spam and cover the computational costs of maintaining the network, ensuring account integrity and discouraging frivolous account creation. For each OwnerCount, an additional {{ownerReserveCostInXrpPerItem}} XRP is required.", "balanceInfoExplanation2": "The reserve can only be recovered by deleting the account from the XRPL.", "knowMore": "Know more", "reviewTxTitle": "Review your transaction", @@ -69,14 +69,14 @@ "visiteSite": "Visit {{name}}", "transaction": "Transaction", "accountNotActiveTitle": "One more step... activate your account.", - "accountNotActiveText": "We need you to activate your account to start using the wallet. Deposit at least 10 XRP in your wallet and you will have the wallet full operative.", + "accountNotActiveText": "We need you to activate your account to start using the wallet. Deposit at least {{baseReserveCostInXrp}} XRP in your wallet and you will have the wallet full operative.", "accountNotActiveCTA": "Active now", "activateAccountAlertTitle": "Why is activation necessary?", - "activateAccountAlertText": "In the XRP Ledger (XRPL), a minimum of 10 XRP must be sent to a new account to activate it. This requirement is primarily for security and efficiency reasons.", + "activateAccountAlertText": "In the XRP Ledger (XRPL), a minimum of {{baseReserveCostInXrp}} XRP must be sent to a new account to activate it. This requirement is primarily for security and efficiency reasons.", "activateAccountText": "You can activate it right now by sending at least ", "activateAccountText2": "to your address, which you can obtain by copying it below or by scanning the QR code. ", - "inviteToBuyText": "Also, you can buy 10 XRP right now", - "buyXRPCTAButton": "Buy now 10 XRP with credit card", + "inviteToBuyText": "Also, you can buy {{baseReserveCostInXrp}} XRP right now", + "buyXRPCTAButton": "Buy now {{baseReserveCostInXrp}} XRP with credit card", "activateAccoutModalTitle": "Activate account", "inviteToGoToFaucetText": "Also, you can get XRP for free from the faucet", "goToFaucetCTAButton": "Go to faucet", diff --git a/packages/site/src/ui/network/hooks/useNetworkReserve.ts b/packages/site/src/ui/network/hooks/useNetworkReserve.ts new file mode 100644 index 00000000..3d1005fd --- /dev/null +++ b/packages/site/src/ui/network/hooks/useNetworkReserve.ts @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; +import { NetworkReserve } from 'common/models'; +import ControllerFactory from 'ui/adapter/ControllerFactory'; +import useSnapState from 'ui/adapter/state/useSnapState'; +import useGetActiveNetwork from 'ui/network/query/useGetActiveNetwork'; +import { Queries } from 'ui/query/queries'; +import type { QueryOptions, QueryResult } from 'ui/query/react-query-overrides'; + +export default function useNetworkReserve({ + enabled = true, + ...options +}: Omit< + QueryOptions, + 'refetchInterval' | '' +> = {}): QueryResult { + const { data: network } = useGetActiveNetwork(); + const { isSnapInstalled } = useSnapState(); + + return useQuery({ + enabled: enabled && isSnapInstalled && Boolean(network), + queryKey: [Queries.GET_NETWORK_RESERVE, network?.chainId], + queryFn: async () => ControllerFactory.networkController.getNetworkReserve(), + ...options, + }); +} diff --git a/packages/site/src/ui/query/queries.ts b/packages/site/src/ui/query/queries.ts index 02cf80c8..396efc3f 100644 --- a/packages/site/src/ui/query/queries.ts +++ b/packages/site/src/ui/query/queries.ts @@ -10,4 +10,5 @@ export enum Queries { GET_XRP_PRICE = 'get-xrp-price', GET_TOKEN_INFO = 'get-token-info', GET_PROMO_CODE = 'get-promo-code', + GET_NETWORK_RESERVE = 'get-network-reserve', } diff --git a/packages/site/src/ui/wallet/containers/AccountNotActive/AccountNotActive.tsx b/packages/site/src/ui/wallet/containers/AccountNotActive/AccountNotActive.tsx index 3755ab3e..f3c571d5 100644 --- a/packages/site/src/ui/wallet/containers/AccountNotActive/AccountNotActive.tsx +++ b/packages/site/src/ui/wallet/containers/AccountNotActive/AccountNotActive.tsx @@ -5,6 +5,7 @@ import Button from 'ui/common/components/input/Button/Button'; import IconCard from 'ui/common/components/surface/IconCard/IconCard'; import { LockIcon } from 'ui/common/icons'; import { useTranslate } from 'ui/locale'; +import useNetworkReserve from 'ui/network/hooks/useNetworkReserve'; import ActivateAccountModal from '../ActivateAccountModal/ActivateAccountModal'; import { AccountNotActiveRoot } from './AccountNotActive.styles'; @@ -17,6 +18,7 @@ export interface AccountNotActiveProps { function AccountNotActive({ className, ...rest }: AccountNotActiveProps) { const translate = useTranslate(); const [modalOpened, setModalOpened] = useState(false); + const { data: { baseReserveCostInXrp } = {} } = useNetworkReserve(); return ( <> @@ -29,7 +31,7 @@ function AccountNotActive({ className, ...rest }: AccountNotActiveProps) { {translate('accountNotActiveTitle')} - {translate('accountNotActiveText')} + {translate('accountNotActiveText', { baseReserveCostInXrp })} ); diff --git a/packages/site/src/ui/wallet/containers/ActivateAccountModal/ActivateAccountModal.tsx b/packages/site/src/ui/wallet/containers/ActivateAccountModal/ActivateAccountModal.tsx index 5443c3f1..839b75e0 100644 --- a/packages/site/src/ui/wallet/containers/ActivateAccountModal/ActivateAccountModal.tsx +++ b/packages/site/src/ui/wallet/containers/ActivateAccountModal/ActivateAccountModal.tsx @@ -1,4 +1,4 @@ -import { Col, Typography, useConfig } from '@peersyst/react-components'; +import { Col, Typography } from '@peersyst/react-components'; import clsx from 'clsx'; import { useTheme } from 'styled-components'; import QrCode from 'ui/common/components/display/QrCode/QrCode'; @@ -7,6 +7,7 @@ import Modal from 'ui/common/components/feedback/Modal/Modal'; import { ModalProps } from 'ui/common/components/feedback/Modal/Modal.types'; import Card from 'ui/common/components/surface/Card/Card'; import { useTranslate } from 'ui/locale'; +import useNetworkReserve from 'ui/network/hooks/useNetworkReserve'; import AccountChip from 'ui/wallet/components/display/AccountChip'; import useGetAddress from 'ui/wallet/hooks/useGetAddress'; @@ -17,8 +18,8 @@ export interface ActivateAccountModalProps extends Omit {} function ActivateAccountModal({ className, children, ...rest }: ActivateAccountModalProps) { const { spacing } = useTheme(); const translate = useTranslate(); - const { baseReserveCostInXrp } = useConfig('xrplNetwork'); const address = useGetAddress() || ''; + const { data: { baseReserveCostInXrp } = {} } = useNetworkReserve(); return ( @@ -29,7 +30,7 @@ function ActivateAccountModal({ className, children, ...rest }: ActivateAccountM {translate('activateAccountAlertTitle')} - {translate('activateAccountAlertText')} + {translate('activateAccountAlertText', { baseReserveCostInXrp })} } diff --git a/packages/site/src/ui/wallet/containers/BalanceDetailsModal/BalanceDetailsModal.tsx b/packages/site/src/ui/wallet/containers/BalanceDetailsModal/BalanceDetailsModal.tsx index 05a5afa0..2ccfd28a 100644 --- a/packages/site/src/ui/wallet/containers/BalanceDetailsModal/BalanceDetailsModal.tsx +++ b/packages/site/src/ui/wallet/containers/BalanceDetailsModal/BalanceDetailsModal.tsx @@ -4,6 +4,7 @@ import InfoDisplay from 'ui/common/components/display/InfoDisplay/InfoDisplay'; import AlertCallout from 'ui/common/components/feedback/AlertCallout/AlertCallout'; import type { ModalProps } from 'ui/common/components/feedback/Modal/Modal.types'; import ExternalLink from 'ui/common/components/navigation/ExternalLink/ExternalLink'; +import useNetworkReserve from 'ui/network/hooks/useNetworkReserve'; import useGetBalanceInfo from 'ui/wallet/query/useGetBalanceInfo'; import Modal from '../../../common/components/feedback/Modal/Modal'; @@ -14,6 +15,7 @@ function BalanceDetailsModal({ ...rest }: ModalProps) { const reserveInfoLink = useConfig('reserveInfoLink'); const translate = useTranslate(); const { data: { expendable, total, reserve } = {}, isLoading } = useGetBalanceInfo(); + const { data: { baseReserveCostInXrp, ownerReserveCostInXrpPerItem } = {} } = useNetworkReserve(); return ( @@ -46,7 +48,7 @@ function BalanceDetailsModal({ ...rest }: ModalProps) { title={translate('reserve')} content={ {translate('balanceInfoExplanationTitle')} - {translate('balanceInfoExplanation')} {`${translate('knowMore')}.`} + {translate('balanceInfoExplanation', { baseReserveCostInXrp, ownerReserveCostInXrpPerItem })}{' '} + {`${translate('knowMore')}.`}