Skip to content

Commit

Permalink
Initial implementation of stake form (#93)
Browse files Browse the repository at this point in the history
This PR adds a stake form. The data after approving the form will be
saved in the context to use the given amount in a later step. The form
for ustake and stake are very similar, so let's create a form base
component. It will allow us to use it for unstake flow later. Changes:

- Added styles for the `Tabs` component
- Created the form base component
- Handling of the stake form
- Basic validation for form input
- Added necessary methods that convert numbers from floating point
number to bigint
  • Loading branch information
r-czajkowski authored Jan 10, 2024
2 parents 3324435 + 597a890 commit 5916950
Show file tree
Hide file tree
Showing 37 changed files with 747 additions and 153 deletions.
1 change: 1 addition & 0 deletions dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@emotion/styled": "^11.11.0",
"@ledgerhq/wallet-api-client": "^1.5.0",
"@ledgerhq/wallet-api-client-react": "^1.3.0",
"formik": "^2.4.5",
"framer-motion": "^10.16.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion dapp/src/components/Header/ConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function ConnectWallet() {
<HStack display={{ base: "none", md: "flex" }}>
<TextMd color="grey.500">Balance</TextMd>
<CurrencyBalance
currencyType="bitcoin"
currency="bitcoin"
amount={btcAccount?.balance.toString()}
/>
</HStack>
Expand Down
41 changes: 41 additions & 0 deletions dapp/src/components/Modals/ActionForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react"
import {
ModalBody,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
} from "@chakra-ui/react"
import StakeForm from "../Staking/StakeForm"
import { useModalFlowContext } from "../../../hooks"

const TABS = ["stake", "unstake"] as const

type Action = (typeof TABS)[number]

function ActionForm({ action }: { action: Action }) {
const { goNext } = useModalFlowContext()

return (
<ModalBody>
<Tabs w="100%" variant="underline" defaultIndex={TABS.indexOf(action)}>
<TabList>
{TABS.map((tab) => (
<Tab key={tab} w="50%">
{tab}
</Tab>
))}
</TabList>
<TabPanels>
<TabPanel>
<StakeForm goNext={goNext} />
</TabPanel>
<TabPanel>{/* TODO: Add form for unstake */}</TabPanel>
</TabPanels>
</Tabs>
</ModalBody>
)
}

export default ActionForm
17 changes: 0 additions & 17 deletions dapp/src/components/Modals/Staking/StakeForm.tsx

This file was deleted.

48 changes: 48 additions & 0 deletions dapp/src/components/Modals/Staking/StakeForm/Details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react"
import { List } from "@chakra-ui/react"
import { useTransactionDetails } from "../../../../hooks"
import TransactionDetailsAmountItem from "../../../shared/TransactionDetails/AmountItem"
import { CurrencyType } from "../../../../types"
import { useTokenAmountFormValue } from "../../../shared/TokenAmountForm/TokenAmountFormBase"

function Details({ currency }: { currency: CurrencyType }) {
const value = useTokenAmountFormValue()
const details = useTransactionDetails(value ?? 0n)

return (
<List spacing={3} mt={10}>
<TransactionDetailsAmountItem
label="Amount to be staked"
from={{
currency,
amount: details?.btcAmount,
}}
to={{
currency: "usd",
}}
/>
<TransactionDetailsAmountItem
label="Protocol fee (0.01%)"
from={{
currency,
amount: details?.protocolFee,
}}
to={{
currency: "usd",
}}
/>
<TransactionDetailsAmountItem
label="Approximately staked tokens"
from={{
currency,
amount: details?.estimatedAmount,
}}
to={{
currency: "usd",
}}
/>
</List>
)
}

export default Details
40 changes: 40 additions & 0 deletions dapp/src/components/Modals/Staking/StakeForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useCallback } from "react"
import { Button } from "@chakra-ui/react"
import { BITCOIN_MIN_AMOUNT } from "../../../../constants"
import { ModalStep } from "../../../../contexts"
import { useWalletContext, useTransactionContext } from "../../../../hooks"
import TokenAmountForm from "../../../shared/TokenAmountForm"
import { TokenAmountFormValues } from "../../../shared/TokenAmountForm/TokenAmountFormBase"
import Details from "./Details"

function StakeForm({ goNext }: ModalStep) {
const { btcAccount } = useWalletContext()
const { setTokenAmount } = useTransactionContext()

const handleSubmitForm = useCallback(
(values: TokenAmountFormValues) => {
if (!values.amount) return

setTokenAmount({ amount: values.amount, currency: "bitcoin" })
goNext()
},
[goNext, setTokenAmount],
)

return (
<TokenAmountForm
tokenBalanceInputPlaceholder="BTC"
currency="bitcoin"
tokenBalance={btcAccount?.balance.toString() ?? "0"}
minTokenAmount={BITCOIN_MIN_AMOUNT}
onSubmitForm={handleSubmitForm}
>
<Details currency="bitcoin" />
<Button type="submit" size="lg" width="100%" mt={4}>
Stake
</Button>
</TokenAmountForm>
)
}

export default StakeForm
4 changes: 2 additions & 2 deletions dapp/src/components/Modals/Staking/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react"
import { useModalFlowContext } from "../../../hooks"
import StakeForm from "./StakeForm"
import Overview from "./Overview"
import ModalBase from "../../shared/ModalBase"
import ActionForm from "../ActionForm"
import SignMessage from "./SignMessage"
import DepositBTC from "./DepositBTC"

Expand All @@ -11,7 +11,7 @@ function ActiveStakingStep() {

switch (activeStep) {
case 1:
return <StakeForm />
return <ActionForm action="stake" />
case 2:
return <Overview />
case 3:
Expand Down
14 changes: 7 additions & 7 deletions dapp/src/components/Modals/Support/MissingAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ import {
import { CurrencyType, RequestAccountParams } from "../../../types"
import { TextMd } from "../../shared/Typography"
import Alert from "../../shared/Alert"
import { CURRENCIES_BY_TYPE } from "../../../constants"
import { getCurrencyByType } from "../../../utils"

type MissingAccountProps = {
currencyType: CurrencyType
currency: CurrencyType
icon: typeof Icon
requestAccount: (...params: RequestAccountParams) => Promise<void>
}

export default function MissingAccount({
currencyType,
currency,
icon,
requestAccount,
}: MissingAccountProps) {
const currency = CURRENCIES_BY_TYPE[currencyType]
const { name, symbol } = getCurrencyByType(currency)

return (
<>
<ModalHeader>{currency.name} account not installed</ModalHeader>
<ModalHeader>{name} account not installed</ModalHeader>
<ModalBody>
<Icon as={icon} boxSize={32} my={2} />
<TextMd>
{currency.name} account is required to make transactions for
depositing and staking your {currency.symbol}.
{name} account is required to make transactions for depositing and
staking your {symbol}.
</TextMd>
<Alert>
<TextMd>
Expand Down
4 changes: 2 additions & 2 deletions dapp/src/components/Modals/Support/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function SupportWrapper({
if (!btcAccount)
return (
<MissingAccount
currencyType="bitcoin"
currency="bitcoin"
icon={ConnectBTCAccount}
requestAccount={requestBitcoinAccount}
/>
Expand All @@ -28,7 +28,7 @@ export default function SupportWrapper({
if (!ethAccount)
return (
<MissingAccount
currencyType="ethereum"
currency="ethereum"
icon={ConnectETHAccount}
requestAccount={requestEthereumAccount}
/>
Expand Down
6 changes: 2 additions & 4 deletions dapp/src/components/Overview/PositionDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ export default function PositionDetails(props: CardProps) {
</HStack>
<CurrencyBalanceWithConversion
from={{
currencyType: "bitcoin",
currency: "bitcoin",
amount: "2398567898",
variant: "greater-balance",
}}
to={{
currencyType: "usd",
amount: 419288.98,
shouldBeFormatted: false,
currency: "usd",
size: "lg",
}}
/>
Expand Down
19 changes: 11 additions & 8 deletions dapp/src/components/shared/CurrencyBalance/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useMemo } from "react"
import { Box, useMultiStyleConfig, TextProps } from "@chakra-ui/react"
import { formatTokenAmount, numberToLocaleString } from "../../../utils"
import {
formatTokenAmount,
getCurrencyByType,
numberToLocaleString,
} from "../../../utils"
import { CurrencyType } from "../../../types"
import { CURRENCIES_BY_TYPE } from "../../../constants"

export type CurrencyBalanceProps = {
currencyType: CurrencyType
currency: CurrencyType
amount?: string | number
shouldBeFormatted?: boolean
desiredDecimals?: number
Expand All @@ -14,7 +17,7 @@ export type CurrencyBalanceProps = {
} & TextProps

export function CurrencyBalance({
currencyType,
currency,
amount,
shouldBeFormatted = true,
desiredDecimals = 2,
Expand All @@ -24,23 +27,23 @@ export function CurrencyBalance({
}: CurrencyBalanceProps) {
const styles = useMultiStyleConfig("CurrencyBalance", { size, variant })

const currency = CURRENCIES_BY_TYPE[currencyType]
const { symbol, decimals } = getCurrencyByType(currency)

const balance = useMemo(() => {
const value = amount ?? 0
if (shouldBeFormatted)
return formatTokenAmount(value, currency.decimals, desiredDecimals)
return formatTokenAmount(value, decimals, desiredDecimals)

return numberToLocaleString(value, desiredDecimals)
}, [amount, currency, desiredDecimals, shouldBeFormatted])
}, [amount, decimals, desiredDecimals, shouldBeFormatted])

return (
<Box>
<Box as="span" __css={styles.balance} {...textProps}>
{balance}
</Box>
<Box as="span" __css={styles.symbol} {...textProps}>
{currency.symbol}
{symbol}
</Box>
</Box>
)
Expand Down
17 changes: 15 additions & 2 deletions dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import React from "react"
import React, { useMemo } from "react"
import { CurrencyBalance, CurrencyBalanceProps } from "../CurrencyBalance"

const MOCK_CONVERSION_AMOUNT = 100

export function CurrencyBalanceWithConversion({
from,
to,
}: {
from: CurrencyBalanceProps
to: CurrencyBalanceProps
}) {
// TODO: Make the correct conversion
const conversionAmount = useMemo(() => {
if (!from.amount || BigInt(from.amount) < 0n) return undefined

return MOCK_CONVERSION_AMOUNT
}, [from.amount])

return (
<>
<CurrencyBalance {...from} />
<CurrencyBalance {...to} />
<CurrencyBalance
amount={conversionAmount}
shouldBeFormatted={false}
{...to}
/>
</>
)
}
4 changes: 4 additions & 0 deletions dapp/src/components/shared/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { chakra } from "@chakra-ui/react"
import { Form as FormikForm } from "formik"

export const Form = chakra(FormikForm)
24 changes: 24 additions & 0 deletions dapp/src/components/shared/Form/FormTokenBalanceInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react"
import { useField } from "formik"
import TokenBalanceInput, { TokenBalanceInputProps } from "../TokenBalanceInput"

export type FormTokenBalanceInputProps = {
name: string
} & Omit<TokenBalanceInputProps, "setAmount">
export function FormTokenBalanceInput({
name,
...restProps
}: FormTokenBalanceInputProps) {
const [field, meta, helpers] = useField(name)

return (
<TokenBalanceInput
{...restProps}
{...field}
amount={meta.value}
setAmount={helpers.setValue}
hasError={Boolean(meta.touched && meta.error)}
errorMsgText={meta.error}
/>
)
}
2 changes: 2 additions & 0 deletions dapp/src/components/shared/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Form"
export * from "./FormTokenBalanceInput"
Loading

0 comments on commit 5916950

Please sign in to comment.