Skip to content

Commit

Permalink
Withdrawals (#474)
Browse files Browse the repository at this point in the history
Depends on: #472 
Closes: #353

This PR adds support for withdrawals in SDK and Acre dapp. To request a
redemption we need to implement our custom redeemer proxy. Our redeemer
proxy builds the redemption data and sends the redemption request
transaction via Orangekit SDK.

Here we also hardcode the `ethers` version to `6.10.0` (without `^`) in
the `solidity` and `sdk` workspaces. The version of `ethers` in the
`solidity` workspace has been resolved to `6.13.1` and the `sdk`
workspace depends on it, so pnpm resolves `ethers` in `sdk` to `6.13.1`
under the hood where there are some changes in the API and causes tests
and builds to fail.
  • Loading branch information
kkosiorowska authored Jun 28, 2024
2 parents 20bd476 + 2421e74 commit 8ae1ec6
Show file tree
Hide file tree
Showing 38 changed files with 4,253 additions and 3,507 deletions.
29 changes: 18 additions & 11 deletions dapp/src/acre-react/contexts/AcreSdkContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BITCOIN_NETWORK } from "#/constants"

const TBTC_API_ENDPOINT = import.meta.env.VITE_TBTC_API_ENDPOINT
const ETH_RPC_URL = import.meta.env.VITE_ETH_HOSTNAME_HTTP
const GELATO_API_KEY = import.meta.env.VITE_GELATO_RELAY_API_KEY

type AcreSdkContextValue = {
acre?: Acre
Expand All @@ -25,19 +26,25 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) {
const [isInitialized, setIsInitialized] = useState(false)
const [isConnected, setIsConnected] = useState(false)

const init = useCallback(async (bitcoinProvider?: BitcoinProvider) => {
let sdk: Acre
const init = useCallback<AcreSdkContextValue["init"]>(
async (bitcoinProvider?: BitcoinProvider) => {
let sdk = await Acre.initialize(
BITCOIN_NETWORK,
TBTC_API_ENDPOINT,
ETH_RPC_URL,
GELATO_API_KEY,
)

sdk = await Acre.initialize(BITCOIN_NETWORK, TBTC_API_ENDPOINT, ETH_RPC_URL)
if (bitcoinProvider) {
sdk = await sdk.connect(bitcoinProvider)
setIsConnected(true)
}

if (bitcoinProvider) {
sdk = await sdk.connect(bitcoinProvider)
setIsConnected(true)
}

setAcre(sdk)
setIsInitialized(true)
}, [])
setAcre(sdk)
setIsInitialized(true)
},
[],
)

const context = useMemo(
() => ({
Expand Down
1 change: 1 addition & 0 deletions dapp/src/acre-react/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./useAcreContext"
export * from "./useStakeFlow"
export { default as useInitializeWithdraw } from "./useInitializeWithdraw"
27 changes: 27 additions & 0 deletions dapp/src/acre-react/hooks/useInitializeWithdraw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useCallback } from "react"
import {
MessageSignedStepCallback,
OnSignMessageStepCallback,
} from "@acre-btc/sdk/dist/src/lib/redeemer-proxy"
import { useAcreContext } from "./useAcreContext"

export default function useInitializeWithdraw() {
const { acre, isConnected } = useAcreContext()

return useCallback(
async (
amount: bigint,
onSignMessageStep?: OnSignMessageStepCallback,
messageSignedStep?: MessageSignedStepCallback,
) => {
if (!acre || !isConnected) return

await acre.account.initializeWithdrawal(
amount,
onSignMessageStep,
messageSignedStep,
)
},
[acre, isConnected],
)
}
1 change: 0 additions & 1 deletion dapp/src/components/TransactionModal/ActionFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ function ActionFormModal({ type }: { type: ActionFlowType }) {

try {
setIsLoading(true)
// TODO: Init unstake flow
if (type === ACTION_FLOW_TYPES.STAKE) await handleInitStake()

dispatch(setTokenAmount({ amount: values.amount, currency: "bitcoin" }))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,105 @@
import React, { useCallback } from "react"
import { useAppDispatch, useExecuteFunction, useModal } from "#/hooks"
import React, { useCallback, useState } from "react"
import {
useActionFlowPause,
useActionFlowTokenAmount,
useAppDispatch,
useExecuteFunction,
useModal,
} from "#/hooks"
import { PROCESS_STATUSES } from "#/types"
import { Button } from "@chakra-ui/react"
import { logPromiseFailure } from "#/utils"
import { eip1193, logPromiseFailure } from "#/utils"
import { setStatus } from "#/store/action-flow"
import { useInitializeWithdraw } from "#/acre-react/hooks"
import TriggerTransactionModal from "../TriggerTransactionModal"

type WithdrawalStatus = "building-data" | "signature" | "transaction"

const withdrawalStatusToContent: Record<
WithdrawalStatus,
{ title: string; subtitle: string }
> = {
"building-data": {
title: "Building transaction data...",
subtitle: "We are building your withdrawal data.",
},
signature: {
title: "Waiting signature...",
subtitle: "Please complete the signing process in your wallet.",
},
transaction: {
title: "Waiting for withdrawal initialization...",
subtitle: "Withdrawal initialization in progress...",
},
}

export default function SignMessageModal() {
const [status, setWaitingStatus] = useState<WithdrawalStatus>("building-data")

const dispatch = useAppDispatch()
const tokenAmount = useActionFlowTokenAmount()
const amount = tokenAmount?.amount
const { closeModal } = useModal()
const { handlePause } = useActionFlowPause()

const initializeWithdraw = useInitializeWithdraw()

const onSignMessageCallback = useCallback(async () => {
setWaitingStatus("signature")
return Promise.resolve()
}, [])

const messageSignedCallback = useCallback(() => {
setWaitingStatus("transaction")
dispatch(setStatus(PROCESS_STATUSES.LOADING))
return Promise.resolve()
}, [dispatch])

const onSignMessageSuccess = useCallback(() => {
dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED))
}, [dispatch])

// TODO: After a failed attempt, we should display the message
const onSignMessageError = useCallback(() => {
dispatch(setStatus(PROCESS_STATUSES.FAILED))
}, [dispatch])

const onError = useCallback(
(error: unknown) => {
if (eip1193.didUserRejectRequest(error)) {
handlePause()
} else {
onSignMessageError()
}
},
[onSignMessageError, handlePause],
)

const handleSignMessage = useExecuteFunction(
// TODO: Use a correct function from the SDK
async () => {},
async () => {
if (!amount) return

await initializeWithdraw(
amount,
onSignMessageCallback,
messageSignedCallback,
)
},
onSignMessageSuccess,
onSignMessageError,
onError,
)

const handleSignMessageWrapper = useCallback(() => {
dispatch(setStatus(PROCESS_STATUSES.LOADING))
const handleInitWithdrawAndSignMessageWrapper = useCallback(() => {
logPromiseFailure(handleSignMessage())
}, [handleSignMessage])

// TODO: Remove when SDK is ready
setTimeout(() => {
logPromiseFailure(handleSignMessage())
}, 5000)
}, [dispatch, handleSignMessage])
const { title, subtitle } = withdrawalStatusToContent[status]

return (
<TriggerTransactionModal callback={handleSignMessageWrapper}>
<TriggerTransactionModal
title={title}
subtitle={subtitle}
callback={handleInitWithdrawAndSignMessageWrapper}
>
<Button size="lg" width="100%" variant="outline" onClick={closeModal}>
Cancel
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react"
import {
Button,
Icon,
Link,
ModalBody,
ModalCloseButton,
ModalFooter,
ModalHeader,
} from "@chakra-ui/react"
import { TextMd } from "#/components/shared/Typography"
import { EXTERNAL_HREF } from "#/constants"
import { IconBrandDiscordFilled } from "@tabler/icons-react"

export default function UnstakeErrorModal() {
return (
<>
<ModalCloseButton />
<ModalHeader color="red.400" textAlign="center">
Unexpected error...
</ModalHeader>
<ModalBody gap={10} pb={6}>
<TextMd>Please try agin.</TextMd>
</ModalBody>
<ModalFooter py={6} px={8} flexDirection="row">
<Button
as={Link}
size="lg"
width="100%"
variant="outline"
rightIcon={<Icon as={IconBrandDiscordFilled} boxSize={5} />}
href={EXTERNAL_HREF.DISCORD}
isExternal
>
Get help on Discord
</Button>
</ModalFooter>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Flex, List } from "@chakra-ui/react"
import TransactionDetailsAmountItem from "#/components/shared/TransactionDetails/AmountItem"
import { useTokenAmountField } from "#/components/shared/TokenAmountForm/TokenAmountFormBase"
import { useTransactionDetails } from "#/hooks"
import { CurrencyType } from "#/types"
import { ACTION_FLOW_TYPES, CurrencyType } from "#/types"
import { featureFlags } from "#/constants"
import WithdrawWarning from "./WithdrawWarning"

Expand All @@ -17,7 +17,7 @@ function UnstakeDetails({
const { value, isValid } = useTokenAmountField()
// Let's not calculate the details of the transaction when the value is not valid.
const amount = isValid ? value : 0n
const details = useTransactionDetails(amount)
const details = useTransactionDetails(amount, ACTION_FLOW_TYPES.UNSTAKE)

return (
<Flex flexDirection="column" gap={10} mt={4}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import TokenAmountForm from "#/components/shared/TokenAmountForm"
import { TokenAmountFormValues } from "#/components/shared/TokenAmountForm/TokenAmountFormBase"
import { FormSubmitButton } from "#/components/shared/Form"
import { BaseFormProps } from "#/types"
import { useEstimatedBTCBalance, useMinDepositAmount } from "#/hooks"
import { useEstimatedBTCBalance, useMinWithdrawAmount } from "#/hooks"
import UnstakeDetails from "./UnstakeDetails"

function UnstakeFormModal({
onSubmitForm,
}: BaseFormProps<TokenAmountFormValues>) {
const balance = useEstimatedBTCBalance()
// TODO: Use the right value from SDK when the logic for the withdraw will be ready
const minTokenAmount = useMinDepositAmount()
const minTokenAmount = useMinWithdrawAmount()

return (
<TokenAmountForm
Expand Down
3 changes: 2 additions & 1 deletion dapp/src/components/TransactionModal/ErrorModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react"
import { ACTION_FLOW_TYPES, ActionFlowType } from "#/types"
import StakingErrorModal from "./ActiveStakingStep/StakingErrorModal"
import UnstakeErrorModal from "./ActiveUnstakingStep/UnstakeErrorModal"

export default function ErrorModal({ type }: { type: ActionFlowType }) {
if (type === ACTION_FLOW_TYPES.STAKE) return <StakingErrorModal />
// TODO: Handle the case of unstake action
if (type === ACTION_FLOW_TYPES.UNSTAKE) return <UnstakeErrorModal />
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ export default function TriggerTransactionModal({
callback,
children,
delay = ONE_SEC_IN_MILLISECONDS,
title = "Waiting transaction...",
subtitle = "Please complete the transaction in your wallet.",
}: {
callback: () => void
children: ReactNode
delay?: number
title?: string
subtitle?: string
}) {
useTimeout(callback, delay)

return (
<>
<ModalHeader>Waiting transaction...</ModalHeader>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<Spinner size="xl" variant="filled" />
<TextMd>Please complete the transaction in your wallet.</TextMd>
<TextMd>{subtitle}</TextMd>
{children}
</ModalBody>
</>
Expand Down
1 change: 1 addition & 0 deletions dapp/src/hooks/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./useInitDataFromSdk"
export * from "./useFetchBTCBalance"
export * from "./useFetchDeposits"
export * from "./useFetchTotalAssets"
export * from "./useMinWithdrawAmount"
4 changes: 4 additions & 0 deletions dapp/src/hooks/sdk/useMinWithdrawAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function useMinWithdrawAmount() {
// TODO: Fetch this amount from SDK.
return 1000000n // 0.01 BTC
}
9 changes: 6 additions & 3 deletions dapp/src/hooks/useTransactionDetails.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"
import { DepositFee } from "#/types"
import { ACTION_FLOW_TYPES, ActionFlowType, DepositFee } from "#/types"
import { initialDepositFee, useTransactionFee } from "./useTransactionFee"

type UseTransactionDetailsResult = {
Expand All @@ -14,9 +14,12 @@ const initialTransactionDetails = {
estimatedAmount: 0n,
}

export function useTransactionDetails(amount: bigint | undefined) {
export function useTransactionDetails(
amount: bigint | undefined,
flow: ActionFlowType = ACTION_FLOW_TYPES.STAKE,
) {
// TODO: Temporary solution - Let's update when withdrawal fees are defined
const transactionFee = useTransactionFee(amount)
const transactionFee = useTransactionFee(amount, flow)
const [details, setDetails] = useState<UseTransactionDetailsResult>(
initialTransactionDetails,
)
Expand Down
Loading

0 comments on commit 8ae1ec6

Please sign in to comment.