Skip to content

WIP: First time staker authorization flow (post-stake) #194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Form } from "../../Forms"
import { FC } from "react"
import { FormikErrors, FormikProps, useField, withFormik } from "formik"
import { getErrorsObj, validateAmountInRange } from "../../../utils/forms"
import {
Alert,
AlertIcon,
Badge,
BodyMd,
BodyXs,
Box,
Button,
Card,
HStack,
LabelMd,
} from "@threshold-network/components"
import { useModal } from "../../../hooks/useModal"
import AuthorizationCardNewStaker from "./AuthorizationCardNewStaker"

export type FormValues = {
tbtcAmountToAuthorize: string | number
isTbtcChecked: boolean
randomBeaconAmountToAuthorize: string | number
isRandomBeaconChecked: boolean
}

export interface AuthInputConstraints {
min: string | number
max: string | number
}

interface Props {
handleSubmit: (vals: any) => void
tbtcInputConstraints: AuthInputConstraints
randomBeaconInputConstraints: AuthInputConstraints
}

export const formikWrapper = withFormik<Props, FormValues>({
handleSubmit: (values) => {
console.log("submitted the form", values)
},
mapPropsToValues: (props) => ({
tbtcAmountToAuthorize: props.tbtcInputConstraints.max,
isTbtcChecked: false,
randomBeaconAmountToAuthorize: props.randomBeaconInputConstraints.max,
isRandomBeaconChecked: true,
}),
validate: (values, props) => {
const errors: FormikErrors<FormValues> = {}

errors.tbtcAmountToAuthorize = validateAmountInRange(
values?.tbtcAmountToAuthorize?.toString(),
props.tbtcInputConstraints.max.toString(),
props.tbtcInputConstraints.min.toString()
)

errors.randomBeaconAmountToAuthorize = validateAmountInRange(
values?.tbtcAmountToAuthorize?.toString(),
props.randomBeaconInputConstraints.max.toString(),
props.randomBeaconInputConstraints.min.toString()
)
return getErrorsObj(errors)
},
displayName: "AuthorizationForm",
})

const AppAuthorizationFormNewStaker: FC<Props & FormikProps<FormValues>> = ({
tbtcInputConstraints,
randomBeaconInputConstraints,
}) => {
const { closeModal } = useModal()

const [, { value: isTbtcChecked }] = useField("isTbtcChecked")
const [, { value: isRandomBeaconChecked }] = useField("isRandomBeaconChecked")
const bothAppsChecked = isTbtcChecked && isRandomBeaconChecked

return (
<Form>
<Box bg="brand.50" p={4} borderRadius={6} mb={6}>
<BodyMd mb={4}>tBTC + Random Beacon Rewards Bundle</BodyMd>
<AuthorizationCardNewStaker
min={tbtcInputConstraints.min}
max={tbtcInputConstraints.max}
inputId="amountTbtcToAuthorize"
checkBoxId="isTbtcChecked"
label="tBTC"
mb={6}
/>
<AuthorizationCardNewStaker
min={randomBeaconInputConstraints.min}
max={randomBeaconInputConstraints.max}
inputId="amountRandomBeaconToAuthorize"
checkBoxId="isRandomBeaconChecked"
label="Random Beacon"
mb={6}
/>
{!bothAppsChecked && (
<Alert status="error" size="sm">
<AlertIcon />
<BodyXs>
Note that you need to authorize both apps to earn rewards.
</BodyXs>
</Alert>
)}
</Box>
<Card bg="gray.50" boxShadow="none" mb={6}>
<HStack justifyContent="space-between">
<LabelMd color="gray.500">PRE</LabelMd>
<Badge variant={"subtle"} colorScheme="gray" color={"gray.500"}>
Authorization not required
</Badge>
</HStack>
</Card>
<Box mb={6}>
<Button onClick={closeModal} variant="outline" mr={2}>
Cancel
</Button>
<Button
disabled={isTbtcChecked === false && isRandomBeaconChecked === false}
type="submit"
>
Authorize Selected Apps
</Button>
</Box>
</Form>
)
}

export default formikWrapper(AppAuthorizationFormNewStaker)
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { FC, useMemo } from "react"
import {
BodyMd,
BoxProps,
Card,
Checkbox,
Grid,
GridItem,
BodySm,
Link,
} from "@threshold-network/components"
import { AppAuthorizationInfo } from "../../../pages/Staking/AuthorizeStakingApps/AuthorizeApplicationsCardCheckbox/AppAuthorizationInfo"
import ThresholdCircleBrand from "../../../static/icons/ThresholdCircleBrand"
import { formatTokenAmount } from "../../../utils/formatAmount"
import { FormikTokenBalanceInput } from "../../Forms/FormikTokenBalanceInput"
import { FormControl, HStack } from "@chakra-ui/react"
import { Field, FieldProps, useField } from "formik"
import numeral from "numeral"
import { formatUnits } from "@ethersproject/units"

export interface AuthorizationCardProps extends BoxProps {
max: string | number
min: string | number
inputId: string
checkBoxId: string
label: string
}

export const AuthorizationCardNewStaker: FC<AuthorizationCardProps> = ({
max,
min,
inputId,
checkBoxId,
label,
...restProps
}) => {
const [, { value: inputValue }, { setValue }] = useField(inputId)
const [, { value: checkboxValue }] = useField(checkBoxId)

const percentToBeAuthorized = useMemo(() => {
if (inputValue) {
return (Number(formatUnits(inputValue)) / Number(formatUnits(max))) * 100
}
return 0
}, [inputValue])

return (
<Card
{...restProps}
boxShadow="none"
borderColor={checkboxValue === true ? "brand.500" : undefined}
>
<Grid
gridTemplateAreas={{
base: `
"checkbox checkbox"
"app-info app-info"
"filter-tabs filter-tabs"
"token-amount-form token-amount-form"
`,
sm: `
"checkbox app-info"
"checkbox filter-tabs"
"checkbox token-amount-form"
`,
md: `
"checkbox app-info filter-tabs "
"checkbox token-amount-form token-amount-form"
"checkbox token-amount-form token-amount-form"
`,
}}
gridTemplateColumns={"1fr 18fr"}
gap="3"
p={0}
>
<Field name={checkBoxId}>
{({ field }: FieldProps) => {
const { value } = field
return (
<FormControl>
<Checkbox
isChecked={value}
gridArea="checkbox"
alignSelf={"flex-start"}
justifySelf={"center"}
size="lg"
{...field}
/>
</FormControl>
)
}}
</Field>
<AppAuthorizationInfo
gridArea="app-info"
label={label}
percentageAuthorized={percentToBeAuthorized}
aprPercentage={10}
slashingPercentage={1}
isAuthorizationRequired={true}
separatePercentAuthorized
/>
<GridItem gridArea="token-amount-form" mt={5}>
<FormikTokenBalanceInput
name={inputId}
label={
<HStack justifyContent="space-between">
<BodyMd>Authorized Amount</BodyMd>
<BodyMd>
Remaining Balance:{" "}
{numeral(formatUnits(max)).format("0,0.00")}
</BodyMd>
</HStack>
}
placeholder="Enter amount"
icon={ThresholdCircleBrand}
max={max}
helperText={
<BodySm>
<Link onClick={() => setValue(min)}>Minimum</Link>
{formatTokenAmount(min)} T for ${label}
</BodySm>
}
_disabled={{ bg: "gray.50", border: "none" }}
/>
</GridItem>
</Grid>
</Card>
)
}

export default AuthorizationCardNewStaker
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { FC, useMemo } from "react"
import {
BodyLg,
BodyMd,
H5,
ModalBody,
ModalCloseButton,
ModalHeader,
Stack,
} from "@threshold-network/components"
import InfoBox from "../../InfoBox"
import { BaseModalProps } from "../../../types"
import { StakeData } from "../../../types/staking"
import withBaseModal from "../withBaseModal"
import AppAuthorizationFormNewStaker from "./AppAuthorizationFormNewStaker"
import { useStakingState } from "../../../hooks/useStakingState"
import TokenBalance from "../../TokenBalance"
import { useStakingAppMinAuthorizationAmount } from "../../../hooks/staking-applications"

const AuthorizeStakingApplicationModalNewStaker: FC<
BaseModalProps & { stake: StakeData }
> = () => {
const handleSubmit = (vals: any) => {
console.log("next", vals)
}

const { stakeAmount } = useStakingState()

const tbtcMinAuthAmount = useStakingAppMinAuthorizationAmount("tbtc")

const randomBeaconMinAuthAmount =
useStakingAppMinAuthorizationAmount("randomBeacon")

const tbtcInputConstraints = useMemo(
() => ({
min: tbtcMinAuthAmount,
max: stakeAmount,
}),
[stakeAmount, tbtcMinAuthAmount]
)

const randomBeaconInputConstraints = useMemo(
() => ({
min: randomBeaconMinAuthAmount,
max: stakeAmount,
}),
[stakeAmount, randomBeaconMinAuthAmount]
)

return (
<>
<ModalHeader>Authorize Apps</ModalHeader>
<ModalCloseButton />
<ModalBody>
<InfoBox variant="modal" mb="6">
<H5 mb={4}>
Please authorize Threshold apps to use your stake to earn rewards
</H5>
<BodyLg>
You can authorize 100% of your stake to all the apps and change this
at any time.
</BodyLg>
</InfoBox>
<Stack spacing={6}>
<BodyMd>Total Staked Balance</BodyMd>
<TokenBalance tokenAmount={stakeAmount} tokenSymbol="T" withSymbol />
<AppAuthorizationFormNewStaker
tbtcInputConstraints={tbtcInputConstraints}
randomBeaconInputConstraints={randomBeaconInputConstraints}
handleSubmit={handleSubmit}
/>
</Stack>
</ModalBody>
</>
)
}

export default withBaseModal(AuthorizeStakingApplicationModalNewStaker)
17 changes: 11 additions & 6 deletions src/components/Modal/ConfirmStakingParams/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,17 @@ const ConfirmStakingParamsModal: FC<
const checkIfProviderUsed = useCheckDuplicateProviderAddress()

// stake transaction, opens success modal on success callback
// not needed once MAS is launched
const { stake } = useStakeTransaction((tx) =>
openModal(ModalType.StakeSuccess, {
transactionHash: tx.hash,
})
)
const { stake } = useStakeTransaction((tx) => {
if (featureFlags.MULTI_APP_STAKING) {
openModal(ModalType.StakeSuccess, {
transactionHash: tx.hash,
})
} else {
openModal(ModalType.StakeSuccessOLD, {
transactionHash: tx.hash,
})
}
})

useEffect(() => {
const forceFormValidation = async () => {
Expand Down
Loading