From d353ce7da50262d6a2b8d9677bf2fdb2708db112 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 6 Dec 2023 15:12:05 +0100 Subject: [PATCH 1/3] Separate the logic of number formatting --- dapp/src/utils/numbers.ts | 48 +++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/dapp/src/utils/numbers.ts b/dapp/src/utils/numbers.ts index 5e44e49db..1b354687f 100644 --- a/dapp/src/utils/numbers.ts +++ b/dapp/src/utils/numbers.ts @@ -4,8 +4,6 @@ const toLocaleString = (value: number): string => /** * Convert a fixed point bigint with precision `fixedPointDecimals` to a * floating point number truncated to `desiredDecimals`. - * If `formattedAmount` is less than the minimum amount to display - * for the specified precision return information about this. * * This function is based on the solution used by the Taho extension. * More info: https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L216-L239 @@ -14,11 +12,7 @@ export function bigIntToUserAmount( fixedPoint: bigint, fixedPointDecimals: number, desiredDecimals = 2, -): string { - if (fixedPoint === BigInt(0)) { - return `0.${"0".repeat(desiredDecimals)}` - } - +): number { const fixedPointDesiredDecimalsAmount = fixedPoint / 10n ** BigInt(Math.max(1, fixedPointDecimals - desiredDecimals)) @@ -26,8 +20,39 @@ export function bigIntToUserAmount( const formattedAmount = Number(fixedPointDesiredDecimalsAmount) / 10 ** Math.min(desiredDecimals, fixedPointDecimals) - const minAmountToDisplay = - 1 / 10 ** Math.min(desiredDecimals, fixedPointDecimals) + + return formattedAmount +} + +/** + * Display a token amount correctly with desired decimals. + * The function returns a string with a language-sensitive representation of this number. + * + * - If the amount entered is zero, return the result with the desired decimals. + * For example, 0.00 for a precision of 2. + * - If `formattedAmount` is less than the minimum amount to display + * for the specified precision return information about this. + * For example, <0.01 for a precision of 2. + * - Other amounts are formatted according to the use of the `bigIntToUserAmount` function. + * + */ +export const formatTokenAmount = ( + amount: number | string, + decimals = 18, + desiredDecimals = 2, +) => { + const fixedPoint = BigInt(amount) + + if (fixedPoint === BigInt(0)) { + return `0.${"0".repeat(desiredDecimals)}` + } + + const formattedAmount = bigIntToUserAmount( + fixedPoint, + decimals, + desiredDecimals, + ) + const minAmountToDisplay = 1 / 10 ** Math.min(desiredDecimals, decimals) if (minAmountToDisplay > formattedAmount) { return `<0.${"0".repeat(desiredDecimals - 1)}1` @@ -35,11 +60,6 @@ export function bigIntToUserAmount( return toLocaleString(formattedAmount) } -export const formatTokenAmount = ( - amount: number | string, - decimals = 18, - desiredDecimals = 2, -) => bigIntToUserAmount(BigInt(amount), decimals, desiredDecimals) export const formatSatoshiAmount = ( amount: number | string, From 0ef287541c38a5cfb1c283db9bd2e59b4d6f1435 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 6 Dec 2023 15:41:13 +0100 Subject: [PATCH 2/3] Add a basic input for token balance --- dapp/src/components/Modals/StakeModal.tsx | 24 +++--- .../components/TokenBalanceInput/index.tsx | 78 +++++++++++++++++++ dapp/src/contexts/StakingFlowContext.tsx | 12 ++- dapp/src/static/icons/Alert.tsx | 24 ++++++ dapp/src/static/icons/index.ts | 1 + dapp/src/utils/numbers.ts | 39 ++++++++++ 6 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 dapp/src/components/TokenBalanceInput/index.tsx create mode 100644 dapp/src/static/icons/Alert.tsx diff --git a/dapp/src/components/Modals/StakeModal.tsx b/dapp/src/components/Modals/StakeModal.tsx index 581431e08..3f4aef654 100644 --- a/dapp/src/components/Modals/StakeModal.tsx +++ b/dapp/src/components/Modals/StakeModal.tsx @@ -9,15 +9,13 @@ import { Tab, TabPanels, TabPanel, - NumberInput, - NumberInputField, - NumberInputStepper, } from "@chakra-ui/react" -import { useStakingFlowContext } from "../../hooks" +import { useStakingFlowContext, useWalletContext } from "../../hooks" import BaseModal from "./BaseModal" import { TokenBalance } from "../TokenBalance" import { BITCOIN } from "../../constants" import { TextMd } from "../Typography" +import TokenBalanceInput from "../TokenBalanceInput" function StakeDetails({ text, @@ -41,7 +39,8 @@ function StakeDetails({ } export default function StakeModal() { - const { closeModal } = useStakingFlowContext() + const { amount, setAmount, closeModal } = useStakingFlowContext() + const { btcAccount } = useWalletContext() return ( @@ -55,11 +54,16 @@ export default function StakeModal() { - {/* TODO: Create a custom number input component */} - - - - + {/* TODO: Add a validation */} + setAmount(value)} + /> {/* TODO: Use the real data */} void +}) { + // TODO: Set the correct color + const colorInfo = useColorModeValue("grey.200", "grey.200") + + const tokenBalanceAmount = useMemo( + () => + fixedPointNumberToString(BigInt(tokenBalance || 0), currency.decimals), + [currency.decimals, tokenBalance], + ) + + return ( + + + Amount + + Balance + + + + + onChange(valueString)} + > + + + + + + + + {/* TODO: Add correct text for tooltip */} + + + + {`${usdAmount} ${USD.symbol}`} + + + ) +} diff --git a/dapp/src/contexts/StakingFlowContext.tsx b/dapp/src/contexts/StakingFlowContext.tsx index a58446787..449e5a936 100644 --- a/dapp/src/contexts/StakingFlowContext.tsx +++ b/dapp/src/contexts/StakingFlowContext.tsx @@ -2,15 +2,17 @@ import React, { createContext, useCallback, useMemo, useState } from "react" import { ModalType } from "../types" type StakingFlowContextValue = { - modalType: ModalType | undefined + modalType?: ModalType + amount?: string closeModal: () => void setModalType: React.Dispatch> + setAmount: React.Dispatch> } export const StakingFlowContext = createContext({ - modalType: undefined, setModalType: () => {}, closeModal: () => {}, + setAmount: () => {}, }) export function StakingFlowProvider({ @@ -19,19 +21,23 @@ export function StakingFlowProvider({ children: React.ReactNode }): React.ReactElement { const [modalType, setModalType] = useState(undefined) + const [amount, setAmount] = useState(undefined) const closeModal = useCallback(() => { setModalType(undefined) + setAmount(undefined) }, []) const contextValue: StakingFlowContextValue = useMemo( () => ({ modalType, + amount, closeModal, setModalType, + setAmount, }), - [modalType, closeModal], + [modalType, amount, closeModal], ) return ( diff --git a/dapp/src/static/icons/Alert.tsx b/dapp/src/static/icons/Alert.tsx new file mode 100644 index 000000000..e76365d89 --- /dev/null +++ b/dapp/src/static/icons/Alert.tsx @@ -0,0 +1,24 @@ +import React from "react" +import { createIcon } from "@chakra-ui/react" + +export const Alert = createIcon({ + displayName: "Alert", + viewBox: "0 0 16 16", + path: [ + + + , + + + + + , + ], +}) diff --git a/dapp/src/static/icons/index.ts b/dapp/src/static/icons/index.ts index 66c6e3aa9..6e64faaba 100644 --- a/dapp/src/static/icons/index.ts +++ b/dapp/src/static/icons/index.ts @@ -2,3 +2,4 @@ export * from "./Info" export * from "./Bitcoin" export * from "./Ethereum" export * from "./ChevronRight" +export * from "./Alert" diff --git a/dapp/src/utils/numbers.ts b/dapp/src/utils/numbers.ts index 1b354687f..f292f41b9 100644 --- a/dapp/src/utils/numbers.ts +++ b/dapp/src/utils/numbers.ts @@ -65,3 +65,42 @@ export const formatSatoshiAmount = ( amount: number | string, desiredDecimals = 2, ) => formatTokenAmount(amount, 8, desiredDecimals) + +/** + * Converts a fixed point number with a bigint amount and a decimals field + * indicating the orders of magnitude in `amount` behind the decimal point into + * a string in US decimal format (no thousands separators, . for the decimal + * separator). + * + * Used in cases where precision is critical. + * + * This function is based on the solution used by the Taho extension. + * More info: https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L172-L214 + */ +export function fixedPointNumberToString( + amount: bigint, + decimals: number, + trimTrailingZeros = true, +): string { + const undecimaledAmount = amount.toString() + const preDecimalLength = undecimaledAmount.length - decimals + + const preDecimalCharacters = + preDecimalLength > 0 + ? undecimaledAmount.substring(0, preDecimalLength) + : "0" + const postDecimalCharacters = + "0".repeat(Math.max(-preDecimalLength, 0)) + + undecimaledAmount.substring(preDecimalLength) + + const trimmedPostDecimalCharacters = trimTrailingZeros + ? postDecimalCharacters.replace(/0*$/, "") + : postDecimalCharacters + + const decimalString = + trimmedPostDecimalCharacters.length > 0 + ? `.${trimmedPostDecimalCharacters}` + : "" + + return `${preDecimalCharacters}${decimalString}` +} From 2880e46180524c95ae9df9b216a38f4cb649ab47 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 6 Dec 2023 15:48:49 +0100 Subject: [PATCH 3/3] Make sure all needed accounts are connected After clicking on the stake button, if the user has no accounts connected, should see special modals with connection options. --- .../components/Modals/ConnectWalletModal.tsx | 33 +++++++++++------- dapp/src/components/Staking/index.tsx | 27 ++++++++++++-- dapp/src/static/images/ConnectETHAccount.png | Bin 0 -> 5301 bytes dapp/src/types/ledger-live-app.ts | 4 ++- 4 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 dapp/src/static/images/ConnectETHAccount.png diff --git a/dapp/src/components/Modals/ConnectWalletModal.tsx b/dapp/src/components/Modals/ConnectWalletModal.tsx index 2658ac432..c0ce3de8a 100644 --- a/dapp/src/components/Modals/ConnectWalletModal.tsx +++ b/dapp/src/components/Modals/ConnectWalletModal.tsx @@ -6,27 +6,34 @@ import { ModalFooter, ModalHeader, VStack, + Text, } from "@chakra-ui/react" -import { useRequestBitcoinAccount } from "../../hooks" import BaseModal from "./BaseModal" -import ConnectBTCAccount from "../../static/images/ConnectBTCAccount.png" -import { TextLg, TextMd } from "../Typography" +import { Currency, RequestAccountParams } from "../../types" -export default function ConnectWalletModal() { - const { requestAccount } = useRequestBitcoinAccount() +type ConnectWalletModalProps = { + currency: Currency + image: string + requestAccount: (...params: RequestAccountParams) => Promise +} +export default function ConnectWalletModal({ + currency, + image, + requestAccount, +}: ConnectWalletModalProps) { return ( - Bitcoin account not installed + {currency.name} account not installed - - - - - Bitcoin account is required to make transactions for depositing and - staking your BTC. - + + + + + {currency.name} account is required to make transactions + for depositing and staking your {currency.symbol}. + diff --git a/dapp/src/components/Staking/index.tsx b/dapp/src/components/Staking/index.tsx index ba6ed6356..b3e42621b 100644 --- a/dapp/src/components/Staking/index.tsx +++ b/dapp/src/components/Staking/index.tsx @@ -1,19 +1,40 @@ import React from "react" import ConnectWalletModal from "../Modals/ConnectWalletModal" import StakingOverviewModal from "../Modals/StakingOverviewModal" -import { useStakingFlowContext, useWalletContext } from "../../hooks" +import { useRequestBitcoinAccount, useRequestEthereumAccount, useStakingFlowContext, useWalletContext } from "../../hooks" import ModalOverlay from "../ModalOverlay" import { HEADER_HEIGHT } from "../Header" import Sidebar from "../Sidebar" import StakeModal from "../Modals/StakeModal" +import { BITCOIN, ETHEREUM } from "../../constants" +import ConnectBTCAccount from "../../static/images/ConnectBTCAccount.png" +import ConnectETHAccount from "../../static/images/ConnectETHAccount.png" function Modal() { const { modalType } = useStakingFlowContext() - const { btcAccount } = useWalletContext() + const { btcAccount, ethAccount } = useWalletContext() + const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() + const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() if (!modalType) return null - if (!btcAccount) return + if (!btcAccount) + return ( + + ) + + if (!ethAccount) + return ( + + ) if (modalType === "overview") return diff --git a/dapp/src/static/images/ConnectETHAccount.png b/dapp/src/static/images/ConnectETHAccount.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ad2974ddd2f2a15094405189d8391ffb76ffe6 GIT binary patch literal 5301 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 Hu0mjf@k38l literal 0 HcmV?d00001 diff --git a/dapp/src/types/ledger-live-app.ts b/dapp/src/types/ledger-live-app.ts index c63368d3e..8e82cb1a3 100644 --- a/dapp/src/types/ledger-live-app.ts +++ b/dapp/src/types/ledger-live-app.ts @@ -1,6 +1,8 @@ import { WalletAPIClient } from "@ledgerhq/wallet-api-client" -type RequestAccountParams = Parameters +export type RequestAccountParams = Parameters< + WalletAPIClient["account"]["request"] +> export type UseRequestAccountReturn = { requestAccount: (...params: RequestAccountParams) => Promise