-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #561 from threshold-network/fetch-redemption-details
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
Showing
9 changed files
with
830 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.