Skip to content

Commit

Permalink
Merge pull request #561 from threshold-network/fetch-redemption-details
Browse files Browse the repository at this point in the history
Fetch redemption details

Adds support for fetching the redemption details data from the chain. Based on
that data we render different states of the unminting process on the Redemption
Details page.

## Main changes:

### Add new methods to the `TBTC` interface from `threshold-ts` lib
Add methods that fetch the redemption request and redemption-related events.
Here we also noticed a bug in the `ethers.js` lib- the `ethers.js` lib encodes
the `bytesX` param in the wrong way. It uses the left-padded rule but based on
the Solidity docs it should be a sequence of bytes in X padded with trailing
zero-bytes to a length of 32 bytes(right-padded). See
https://docs.soliditylang.org/en/v0.8.17/abi-spec.html#formal-specification-of-the-encoding
Consider this wallet public key hash
`0x03B74D6893AD46DFDD01B9E0E3B3385F4FCE2D1E`:
- `ethers.js` returns
  `0x00000000000000000000000003b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e`
- should be:
  `0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e000000000000000000000000`
  
In that case, in methods that fetch the past events by indexed param which has
`bytesX` type(`RedemptionsCompleted`, `RedemptionRequested`,
`RedemptionTimedOut`) we build the filter topics manually.

### Hook that fetches the redemption details
This hook fetches the redemption request details based on the:
- **redemption requested tx hash**-  We also need to find an event by
  transaction hash because it's possible that there can be multiple
  `RedemptionRequest` events with the same redemption key but created at
  different times eg:
    - redemption X requested,
    - redemption X was handled successfully and the redemption X was
      removed from `pendingRedemptions` map,
    - the same wallet is still in `live` state and can handle the
      redemption request with the same `walletPubKeyHash` and
      `redeemerOutputScript` pair,
    - now 2 `RedemptionRequested` events exist with the same redemption
      key(the same `walletPubKeyHash` and `redeemerOutputScript` pair).

      In that case, we must know exactly which redemption request we
      want to fetch.
- **wallet public key hash**- we need to find `RedemptionRequested` event by
  wallet public key hash to get all necessary data and make sure that
  the request actually happened. It's also used to build the redemption key,
- **redeemer**-  We need `redeemer` address as well to reduce the number of
  records- any user can request redemption for the same wallet.
- **redeemer output script**- we need this param to build the redemption key
  and find the Bitcoin transfer for this redeemer output script.
  
### Hook that fetches the block details such as `timestamp`
We need timestamps for a given block umber to calculate the total time it took
to complete a redemption or how long a redemption request takes.
  • Loading branch information
michalsmiarowski authored Jul 11, 2023
2 parents aa27e2c + ed3e3c4 commit 40a99f8
Show file tree
Hide file tree
Showing 9 changed files with 830 additions and 168 deletions.
216 changes: 216 additions & 0 deletions src/hooks/tbtc/useFetchRedemptionDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { useEffect, useState } from "react"
import { useThreshold } from "../../contexts/ThresholdContext"
import { prependScriptPubKeyByLength } from "../../threshold-ts/utils"
import { useGetBlock } from "../../web3/hooks"

interface RedemptionDetails {
amount: string
redemptionRequestedTxHash: string
redemptionCompletedTxHash?: {
chain: string
bitcoin: string
}
requestedAt: number
completedAt?: number
isTimedOut: boolean
redemptionTimedOutTxHash?: string
btcAddress?: string
}

export const useFetchRedemptionDetails = (
redemptionRequestedTxHash: string,
walletPublicKeyHash: string,
redeemerOutputScript: string,
redeemer: string
) => {
const threshold = useThreshold()
const getBlock = useGetBlock()
const [isFetching, setIsFetching] = useState(false)
const [error, setError] = useState("")
const [redemptionData, setRedemptionData] = useState<
RedemptionDetails | undefined
>()

useEffect(() => {
const fetch = async () => {
setIsFetching(true)
try {
const redemptionKey = threshold.tbtc.buildRedemptionKey(
walletPublicKeyHash,
redeemerOutputScript
)

// We need to find `RedemptionRequested` event by wallet public key hash
// and `redeemer` address to get all necessary data and make sure that
// the request actually happened. We need `redeemer` address as well to
// reduce the number of records - any user can request redemption for
// the same wallet.
const redemptionRequest = (
await threshold.tbtc.getRedemptionRequestedEvents({
walletPublicKeyHash,
redeemer,
})
).find(
(event) =>
// It's not possible that the redemption request with the same
// redemption key can be created in the same transaction - it means
// that redemption key is unique and can be used for only one
// pending request at the same time. We also need to find an event
// by transaction hash because it's possible that there can be
// multiple `RedemptionRequest` events with the same redemption key
// but created at different times eg:
// - redemption X requested,
// - redemption X was handled successfully and the redemption X was
// removed from `pendingRedemptions` map,
// - the same wallet is still in `live` state and can handle
// redemption request with the same `walletPubKeyHash` and
// `redeemerOutputScript` pair,
// - now 2 `RedemptionRequested` events exist with the same
// redemption key(the same `walletPubKeyHash` and
// `redeemerOutputScript` pair).
//
// In that case we must know exactly which redemption request we
// want to fetch.
event.txHash === redemptionRequestedTxHash &&
threshold.tbtc.buildRedemptionKey(
event.walletPublicKeyHash,
event.redeemerOutputScript
) === redemptionKey
)

if (!redemptionRequest) {
throw new Error("Redemption not found...")
}

const { timestamp: redemptionRequestedEventTimestamp } = await getBlock(
redemptionRequest.blockNumber
)

// We need to check if the redemption has `pending` or `timedOut` status.
const { isPending, isTimedOut, requestedAt } =
await threshold.tbtc.getRedemptionRequest(
threshold.tbtc.buildRedemptionKey(
walletPublicKeyHash,
redeemerOutputScript
)
)

// Find the transaction hash where the timeout was reported by
// scanning the `RedemptionTimedOut` event by the `walletPubKeyHash`
// param.
const timedOutTxHash: undefined | string = isTimedOut
? (
await threshold.tbtc.getRedemptionTimedOutEvents({
walletPublicKeyHash,
fromBlock: redemptionRequest.blockNumber,
})
).find(
(event) => event.redeemerOutputScript === redeemerOutputScript
)?.txHash
: undefined

if (
(isTimedOut || isPending) &&
// We need to make sure this is the same redemption request. Let's
// consider this case:
// - redemption X requested,
// - redemption X was handled successfully and the redemption X was
// removed from `pendingRedemptions` map,
// - the same wallet is still in `live` state and can handle
// redemption request with the same `walletPubKeyHash` and
// `redeemerOutputScript` pair(the same redemption request key),
// - the redemption request X exists in the `pendingRedemptions` map.
//
// In that case we want to fetch redemption data for the first
// request, so we must compare timestamps, otherwise the redemption
// will be considered as pending.
requestedAt === redemptionRequestedEventTimestamp
) {
setRedemptionData({
amount: redemptionRequest.amount,
redemptionRequestedTxHash: redemptionRequest.txHash,
redemptionCompletedTxHash: undefined,
requestedAt: requestedAt,
redemptionTimedOutTxHash: timedOutTxHash,
isTimedOut,
})
return
}

// If we are here it means that the redemption request was handled
// successfully and we need to find all `RedemptionCompleted` events
// that happened after `redemptionRequest` block and filter by
// `walletPubKeyHash` param.
const redemptionCompletedEvents =
await threshold.tbtc.getRedemptionsCompletedEvents({
walletPublicKeyHash,
fromBlock: redemptionRequest.blockNumber,
})

// For each event we should take `redemptionTxHash` param from
// `RedemptionCompleted` event and check if in that Bitcoin transaction
// we can find transfer to a `redeemerOutputScript` using
// `bitcoinClient.getTransaction`.
for (const {
redemptionBitcoinTxHash,
txHash,
blockNumber: redemptionCompletedBlockNumber,
} of redemptionCompletedEvents) {
const { outputs } = await threshold.tbtc.getBitcoinTransaction(
redemptionBitcoinTxHash
)

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

const { timestamp: redemptionCompletedTimestamp } = await getBlock(
redemptionCompletedBlockNumber
)
setRedemptionData({
amount: redemptionRequest.amount,
redemptionRequestedTxHash: redemptionRequest.txHash,
redemptionCompletedTxHash: {
chain: txHash,
bitcoin: redemptionBitcoinTxHash,
},
requestedAt: redemptionRequestedEventTimestamp,
completedAt: redemptionCompletedTimestamp,
isTimedOut: false,
// TODO: convert the `scriptPubKey` to address.
btcAddress: "2Mzs2YNphdHmBoE7SE77cGB57JBXveNGtae",
})

return
}
}
} catch (error) {
console.error("Could not fetch the redemption request details!", error)
setError((error as Error).toString())
} finally {
setIsFetching(false)
}
}

if (
redemptionRequestedTxHash &&
walletPublicKeyHash &&
redeemer &&
redeemerOutputScript
) {
fetch()
}
}, [
redemptionRequestedTxHash,
walletPublicKeyHash,
redeemer,
redeemerOutputScript,
threshold,
getBlock,
])

return { isFetching, data: redemptionData, error }
}
19 changes: 4 additions & 15 deletions src/pages/tBTC/Bridge/DepositDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { CurveFactoryPoolId, ExternalHref } from "../../../enums"
import { ExternalPool } from "../../../components/tBTC/ExternalPool"
import { useFetchExternalPoolData } from "../../../hooks/useFetchExternalPoolData"
import { TransactionDetailsAmountItem } from "../../../components/TransacionDetails"
import { BridgeProcessDetailsPageSkeleton } from "./components/BridgeProcessDetailsPageSkeleton"

export const DepositDetails: PageComponent = () => {
const { depositKey } = useParams()
Expand Down Expand Up @@ -189,7 +190,9 @@ export const DepositDetails: PageComponent = () => {
<BridgeProcessDetailsCard
isProcessCompleted={mintingProgressStep === "completed"}
>
{(isFetching || !data) && !error && <DepositDetailsPageSkeleton />}
{(isFetching || !data) && !error && (
<BridgeProcessDetailsPageSkeleton />
)}
{error && <>{error}</>}
{!isFetching && !!data && !error && (
<>
Expand Down Expand Up @@ -347,20 +350,6 @@ const useDepositDetailsPageContext = () => {
return context
}

const DepositDetailsPageSkeleton: FC = () => {
return (
<>
<SkeletonText noOfLines={1} skeletonHeight={6} />

<Skeleton height="80px" mt="4" />

<SkeletonText noOfLines={1} width="40%" skeletonHeight={6} mt="8" />
<SkeletonCircle mt="4" size="160px" mx="auto" />
<SkeletonText mt="4" noOfLines={4} spacing={2} skeletonHeight={4} />
</>
)
}

type DepositDetailsTimelineStep =
| "bitcoin-confirmations"
| "minting-initialized"
Expand Down
Loading

0 comments on commit 40a99f8

Please sign in to comment.