Skip to content

Commit

Permalink
Merge pull request #594 from threshold-network/redemption-details-imp…
Browse files Browse the repository at this point in the history
…rovements

Improve UX in redemption details page

Redirect a user to a success step automatically when the redemption is complete.
Here we subscribe to the RedemptionsCompleted event and check if this event is
related to a given redemption request by checking the wallet public key hash. If
yes, we are looking for a Bitcoin transfer to a given redeemer output script in
a given bitcoin tx hash (we can get this transaction hash from the
RedemptionsCompleted event). If we are able to find that transfer it means this
RedemptionsCompleted event is related to a given redemption request and we
should redirect user to a success step and update the bridge activity list.
  • Loading branch information
michalsmiarowski authored Sep 12, 2023
2 parents bfa11b9 + 1881695 commit 85f8536
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 50 deletions.
2 changes: 2 additions & 0 deletions src/hooks/tbtc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export * from "./useSubscribeToRedemptionRequestedEvent"
export * from "./useSubsribeToDepositRevealedEvent"
export * from "./useTBTCDepositDataFromLocalStorage"
export * from "./useTBTCVaultContract"
export * from "./useSubscribeToRedemptionsCompletedEvent"
export * from "./useFindRedemptionInBitcoinTx"
68 changes: 31 additions & 37 deletions src/hooks/tbtc/useFetchRedemptionDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { BigNumber } from "ethers"
import { useEffect, useState } from "react"
import { useThreshold } from "../../contexts/ThresholdContext"
import {
createAddressFromOutputScript,
prependScriptPubKeyByLength,
isValidType,
fromSatoshiToTokenPrecision,
} from "../../threshold-ts/utils"
import { useGetBlock } from "../../web3/hooks"
import { isEmptyOrZeroAddress } from "../../web3/utils"
import { useFindRedemptionInBitcoinTx } from "./useFindRedemptionInBitcoinTx"

interface RedemptionDetails {
requestedAmount: string // in token precision
Expand Down Expand Up @@ -36,6 +35,7 @@ export const useFetchRedemptionDetails = (
) => {
const threshold = useThreshold()
const getBlock = useGetBlock()
const findRedemptionInBitcoinTx = useFindRedemptionInBitcoinTx()
const [isFetching, setIsFetching] = useState(false)
const [error, setError] = useState("")
const [redemptionData, setRedemptionData] = useState<
Expand Down Expand Up @@ -194,44 +194,37 @@ export const useFetchRedemptionDetails = (
txHash,
blockNumber: redemptionCompletedBlockNumber,
} of redemptionCompletedEvents) {
const { outputs } = await threshold.tbtc.getBitcoinTransaction(
redemptionBitcoinTxHash
const redemptionBitcoinTransfer = await findRedemptionInBitcoinTx(
redemptionBitcoinTxHash,
redemptionCompletedBlockNumber,
redemptionRequestedEvent.redeemerOutputScript
)

for (const { scriptPubKey, value } of outputs) {
if (
prependScriptPubKeyByLength(scriptPubKey.toString()) !==
redemptionRequestedEvent.redeemerOutputScript
)
continue
if (!redemptionBitcoinTransfer) continue

const { timestamp: redemptionCompletedTimestamp } = await getBlock(
redemptionCompletedBlockNumber
)
setRedemptionData({
requestedAmount: fromSatoshiToTokenPrecision(
redemptionRequestedEvent.amount
).toString(),
receivedAmount: value.toString(),
redemptionRequestedTxHash: redemptionRequestedEvent.txHash,
redemptionCompletedTxHash: {
chain: txHash,
bitcoin: redemptionBitcoinTxHash,
},
requestedAt: redemptionRequestedEventTimestamp,
completedAt: redemptionCompletedTimestamp,
treasuryFee: fromSatoshiToTokenPrecision(
redemptionRequestedEvent.treasuryFee
).toString(),
isTimedOut: false,
btcAddress: createAddressFromOutputScript(
scriptPubKey,
threshold.tbtc.bitcoinNetwork
),
})

return
}
const { receivedAmount, redemptionCompletedTimestamp, btcAddress } =
redemptionBitcoinTransfer

setRedemptionData({
requestedAmount: fromSatoshiToTokenPrecision(
redemptionRequestedEvent.amount
).toString(),
receivedAmount,
redemptionRequestedTxHash: redemptionRequestedEvent.txHash,
redemptionCompletedTxHash: {
chain: txHash,
bitcoin: redemptionBitcoinTxHash,
},
requestedAt: redemptionRequestedEventTimestamp,
completedAt: redemptionCompletedTimestamp,
treasuryFee: fromSatoshiToTokenPrecision(
redemptionRequestedEvent.treasuryFee
).toString(),
isTimedOut: false,
btcAddress,
})

return
}
} catch (error) {
console.error("Could not fetch the redemption request details!", error)
Expand All @@ -249,6 +242,7 @@ export const useFetchRedemptionDetails = (
redeemerOutputScript,
threshold,
getBlock,
findRedemptionInBitcoinTx,
])

return { isFetching, data: redemptionData, error }
Expand Down
47 changes: 47 additions & 0 deletions src/hooks/tbtc/useFindRedemptionInBitcoinTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useCallback } from "react"
import { useThreshold } from "../../contexts/ThresholdContext"
import {
createAddressFromOutputScript,
prependScriptPubKeyByLength,
} from "../../threshold-ts/utils"
import { useGetBlock } from "../../web3/hooks"

export const useFindRedemptionInBitcoinTx = () => {
const threshold = useThreshold()
const getBlock = useGetBlock()

return useCallback(
async (
redemptionBitcoinTxHash: string,
redemptionCompletedBlockNumber: number,
redeemerOutputScript: string
) => {
const { outputs } = await threshold.tbtc.getBitcoinTransaction(
redemptionBitcoinTxHash
)

for (const { scriptPubKey, value } of outputs) {
if (
prependScriptPubKeyByLength(scriptPubKey.toString()) !==
redeemerOutputScript
)
continue

const { timestamp: redemptionCompletedTimestamp } = await getBlock(
redemptionCompletedBlockNumber
)

return {
btcAddress: createAddressFromOutputScript(
scriptPubKey,
threshold.tbtc.bitcoinNetwork
),
receivedAmount: value.toString(),
bitcoinTxHash: redemptionBitcoinTxHash,
redemptionCompletedTimestamp,
}
}
},
[threshold, getBlock]
)
}
33 changes: 33 additions & 0 deletions src/hooks/tbtc/useSubscribeToRedemptionsCompletedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Hex } from "@keep-network/tbtc-v2.ts"
import { Event } from "ethers"
import { useSubscribeToContractEvent } from "../../web3/hooks"
import { useBridgeContract } from "./useBridgeContract"

type RedemptionsCompletedEventCallback = (
walletPublicKeyHash: string,
redemptionTxHash: string,
event: Event
) => void

export const useSubscribeToRedemptionsCompletedEventBase = (
callback: RedemptionsCompletedEventCallback,
filterParams?: any[],
shouldSubscribeIfUserNotConnected: boolean = false
) => {
const tBTCBridgeContract = useBridgeContract()

useSubscribeToContractEvent(
tBTCBridgeContract,
"RedemptionsCompleted",
//@ts-ignore
(walletPublicKeyHash, redemptionTxHash, event) => {
callback(
walletPublicKeyHash,
Hex.from(redemptionTxHash).reverse().toString(),
event
)
},
filterParams,
shouldSubscribeIfUserNotConnected
)
}
75 changes: 62 additions & 13 deletions src/pages/tBTC/Bridge/UnmintDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,39 +54,84 @@ import { featureFlags } from "../../../constants"
import { useFetchRedemptionDetails } from "../../../hooks/tbtc/useFetchRedemptionDetails"
import { BridgeProcessDetailsPageSkeleton } from "./components/BridgeProcessDetailsPageSkeleton"
import { ExternalHref } from "../../../enums"
import {
useFindRedemptionInBitcoinTx,
useSubscribeToRedemptionsCompletedEventBase,
} from "../../../hooks/tbtc"
import { useAppDispatch } from "../../../hooks/store"
import { tbtcSlice } from "../../../store/tbtc"
import { useThreshold } from "../../../contexts/ThresholdContext"

export const UnmintDetails: PageComponent = () => {
const [searchParams] = useSearchParams()
const walletPublicKeyHash = searchParams.get("walletPublicKeyHash")
const redeemerOutputScript = searchParams.get("redeemerOutputScript")
const redeemer = searchParams.get("redeemer")
const { redemptionRequestedTxHash } = useParams()
const dispatch = useAppDispatch()
const threshold = useThreshold()

const { data, isFetching, error } = useFetchRedemptionDetails(
redemptionRequestedTxHash,
walletPublicKeyHash,
redeemerOutputScript,
redeemer
)
const findRedemptionInBitcoinTx = useFindRedemptionInBitcoinTx()
const [redemptionFromBitcoinTx, setRedemptionFromBitcoinTx] = useState<
Awaited<ReturnType<typeof findRedemptionInBitcoinTx>> | undefined
>(undefined)

useSubscribeToRedemptionsCompletedEventBase(
async (eventWalletPublicKeyHash, redemptionTxHash, event) => {
if (eventWalletPublicKeyHash !== walletPublicKeyHash) return

const redemption = await findRedemptionInBitcoinTx(
redemptionTxHash,
event.blockNumber,
redeemerOutputScript!
)
if (!redemption) return

setRedemptionFromBitcoinTx(redemption)

if (redemptionRequestedTxHash && redeemerOutputScript) {
dispatch(
tbtcSlice.actions.redemptionCompleted({
redemptionKey: threshold.tbtc.buildRedemptionKey(
walletPublicKeyHash,
redeemerOutputScript
),
redemptionRequestedTxHash,
})
)
}
},
[],
true
)

const [shouldDisplaySuccessStep, setShouldDisplaySuccessStep] =
useState(false)

const _isFetching = (isFetching || !data) && !error
const wasDataFetched = !isFetching && !!data && !error

const btcTxHash = data?.redemptionCompletedTxHash?.bitcoin
useEffect(() => {
setShouldDisplaySuccessStep(!!btcTxHash)
}, [btcTxHash])
const isProcessCompleted = !!redemptionFromBitcoinTx?.bitcoinTxHash
const shouldForceIsProcessCompleted =
!!data?.redemptionCompletedTxHash?.bitcoin

const isProcessCompleted = !!data?.redemptionCompletedTxHash?.bitcoin
const requestedAmount = data?.requestedAmount ?? "0"
const receivedAmount = data?.receivedAmount ?? "0"
const receivedAmount =
data?.receivedAmount ?? redemptionFromBitcoinTx?.receivedAmount ?? "0"
const btcTxHash =
data?.redemptionCompletedTxHash?.bitcoin ??
redemptionFromBitcoinTx?.bitcoinTxHash

const thresholdNetworkFee = data?.treasuryFee ?? "0"
const btcAddress = data?.btcAddress
const redemptionCompletedAt = data?.completedAt
const btcAddress = data?.btcAddress ?? redemptionFromBitcoinTx?.btcAddress
const redemptionCompletedAt =
data?.completedAt ?? redemptionFromBitcoinTx?.redemptionCompletedTimestamp
const redemptionRequestedAt = data?.requestedAt
const [redemptionTime, setRedemptionTime] = useState<
ReturnType<typeof dateAs>
Expand Down Expand Up @@ -131,7 +176,7 @@ export const UnmintDetails: PageComponent = () => {
},
{
label: "BTC sent",
txHash: data?.redemptionCompletedTxHash?.bitcoin,
txHash: btcTxHash,
chain: "bitcoin",
},
]
Expand Down Expand Up @@ -204,11 +249,15 @@ export const UnmintDetails: PageComponent = () => {
</TimelineContent>
</TimelineItem>
<TimelineItem
status={isProcessCompleted ? "active" : "semi-active"}
status={
isProcessCompleted || shouldForceIsProcessCompleted
? "active"
: "semi-active"
}
>
<TimelineBreakpoint>
<TimelineDot position="relative">
{isProcessCompleted && (
{(isProcessCompleted || shouldForceIsProcessCompleted) && (
<Icon
as={IoCheckmarkSharp}
position="absolute"
Expand All @@ -229,7 +278,7 @@ export const UnmintDetails: PageComponent = () => {
</TimelineContent>
</TimelineItem>
</Timeline>
{shouldDisplaySuccessStep || isProcessCompleted ? (
{shouldDisplaySuccessStep || shouldForceIsProcessCompleted ? (
<SuccessStep
requestedAmount={requestedAmount}
receivedAmount={receivedAmount}
Expand All @@ -240,7 +289,7 @@ export const UnmintDetails: PageComponent = () => {
<BridgeProcessStep
title="Unminting in progress"
chain="ethereum"
txHash={"0x0"}
txHash={redemptionRequestedTxHash}
progressBarColor="brand.500"
isCompleted={isProcessCompleted}
icon={<ProcessCompletedBrandGradientIcon />}
Expand Down
26 changes: 26 additions & 0 deletions src/store/tbtc/tbtcSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,32 @@ export const tbtcSlice = createSlice({
...state.bridgeActivity.data,
]
},
redemptionCompleted: (
state,
action: PayloadAction<{
redemptionKey: string
redemptionRequestedTxHash: string
}>
) => {
const {
payload: { redemptionKey, redemptionRequestedTxHash },
} = action

const { itemToUpdate, index } = findRedemptionActivity(
state.bridgeActivity.data,
redemptionKey,
redemptionRequestedTxHash
)

if (!itemToUpdate) return

state.bridgeActivity.data[index] = {
...itemToUpdate,
activityKey: redemptionKey,
txHash: redemptionRequestedTxHash,
status: BridgeActivityStatus.UNMINTED,
}
},
},
})

Expand Down

0 comments on commit 85f8536

Please sign in to comment.