diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts new file mode 100644 index 000000000..d0a1b61b4 --- /dev/null +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -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 } +} diff --git a/src/pages/tBTC/Bridge/DepositDetails.tsx b/src/pages/tBTC/Bridge/DepositDetails.tsx index 53180af61..87990de17 100644 --- a/src/pages/tBTC/Bridge/DepositDetails.tsx +++ b/src/pages/tBTC/Bridge/DepositDetails.tsx @@ -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() @@ -189,7 +190,9 @@ export const DepositDetails: PageComponent = () => { - {(isFetching || !data) && !error && } + {(isFetching || !data) && !error && ( + + )} {error && <>{error}} {!isFetching && !!data && !error && ( <> @@ -347,20 +350,6 @@ const useDepositDetailsPageContext = () => { return context } -const DepositDetailsPageSkeleton: FC = () => { - return ( - <> - - - - - - - - - ) -} - type DepositDetailsTimelineStep = | "bitcoin-confirmations" | "minting-initialized" diff --git a/src/pages/tBTC/Bridge/UnmintDetails.tsx b/src/pages/tBTC/Bridge/UnmintDetails.tsx index 467e8dc34..4ff9f59ce 100644 --- a/src/pages/tBTC/Bridge/UnmintDetails.tsx +++ b/src/pages/tBTC/Bridge/UnmintDetails.tsx @@ -14,6 +14,7 @@ import { LabelSm, List, ListItem, + SkeletonText, useColorModeValue, } from "@threshold-network/components" import { @@ -45,10 +46,28 @@ import { } from "./BridgeLayout" import { ExplorerDataType } from "../../../utils/createEtherscanLink" import { PageComponent } from "../../../types" -import { ONE_SEC_IN_MILISECONDS } from "../../../utils/date" +import { dateToUnixTimestamp, dateAs } from "../../../utils/date" import { CopyAddressToClipboard } from "../../../components/CopyToClipboard" import { ProcessCompletedBrandGradientIcon } from "./components/BridgeProcessDetailsIcons" import { featureFlags } from "../../../constants" +import { useFetchRedemptionDetails } from "../../../hooks/tbtc/useFetchRedemptionDetails" +import { BridgeProcessDetailsPageSkeleton } from "./components/BridgeProcessDetailsPageSkeleton" + +const pendingRedemption = { + redemptionRequestedTxHash: + "0xf7d0c92c8de4d117d915c2a8a54ee550047f926bc00b91b651c40628751cfe29", + walletPublicKeyHash: "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + redeemerOutputScript: "0x160014751E76E8199196D454941C45D1B3A323F1433BD6", + redeemer: "0x086813525A7dC7dafFf015Cdf03896Fd276eab60", +} + +const completedRedemption = { + redemptionRequestedTxHash: + "0x0b5d66b89c5fe276ac5b0fd1874142f99329ea6f66485334a558e2bccd977618", + walletPublicKeyHash: "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + redeemerOutputScript: "0x17A91486884E6BE1525DAB5AE0B451BD2C72CEE67DCF4187", + redeemer: "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", +} export const UnmintDetails: PageComponent = () => { // TODO: Fetch redemption details by redemption key. @@ -56,35 +75,55 @@ export const UnmintDetails: PageComponent = () => { const [shouldDisplaySuccessStep, setShouldDisplaySuccessStep] = useState(false) - // TODO: It's a temporary solution to be able to go through the whole flow. - // Remove once we implement the correct solution. - const [isProcessCompleted, setIsProcessCompleted] = useState(false) - useEffect(() => { - const id = setTimeout(() => { - setIsProcessCompleted(true) - }, ONE_SEC_IN_MILISECONDS * 10) + const { + redemptionRequestedTxHash, + walletPublicKeyHash, + redeemerOutputScript, + redeemer, + } = pendingRedemption + + const { data, isFetching, error } = useFetchRedemptionDetails( + redemptionRequestedTxHash, + walletPublicKeyHash, + redeemerOutputScript, + redeemer + ) + + const _isFetching = (isFetching || !data) && !error + const wasDataFetched = !isFetching && !!data && !error - return () => { - clearTimeout(id) - } - }, []) + const btcTxHash = data?.redemptionCompletedTxHash?.bitcoin + useEffect(() => { + if (!!btcTxHash) setShouldDisplaySuccessStep(true) + }, [btcTxHash]) - // TODO: check if the process is completed based on the redemptions details - // data. - // const isProcessCompleted = true - const unmintedAmount = "1200000000000000000" - const btcAddress = "bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h" + const isProcessCompleted = !!data?.redemptionCompletedTxHash?.bitcoin + const unmintedAmount = data?.amount ?? "0" + const btcAddress = data?.btcAddress const fee = "20000000000000000" + const time = dateAs( + (data?.completedAt ?? dateToUnixTimestamp()) - (data?.requestedAt ?? 0) + ) const transactions: { label: string txHash?: string chain: ViewInBlockExplorerChain }[] = [ - { label: "Unwrap", txHash: "0x0", chain: "ethereum" }, - { label: "BTC sent", txHash: "0x1", chain: "bitcoin" }, + { + label: "Unwrap", + txHash: data?.redemptionRequestedTxHash, + chain: "ethereum", + }, + { + label: "BTC sent", + txHash: data?.redemptionCompletedTxHash?.bitcoin, + chain: "bitcoin", + }, ] + const timelineBadgeBgColor = useColorModeValue("white", "brand.800") + return ( { isProcessCompleted={shouldDisplaySuccessStep} > - - - {!shouldDisplaySuccessStep && ( - - {" "} - - In progress... - - )} - - - - - usual duration - 5 hours - - - - - - - - - - tBTC unwrapped - - - - - - {isProcessCompleted && ( - - )} - - - - - BTC sent - - - - {shouldDisplaySuccessStep ? ( - - ) : ( - } - onComplete={() => setShouldDisplaySuccessStep(true)} - isIndeterminate - > - - Your redemption request is being processed. This will take around - 5 hours. - - + {_isFetching && } + {error && <>{error}} + {wasDataFetched && ( + <> + + + {!shouldDisplaySuccessStep && ( + + {" "} + - In progress... + + )} + + + + + usual duration - 5 hours + + + + + + + + + + tBTC unwrapped + + + + + + {isProcessCompleted && ( + + )} + + + + + BTC sent + + + + {shouldDisplaySuccessStep || isProcessCompleted ? ( + + ) : ( + } + onComplete={() => setShouldDisplaySuccessStep(true)} + isIndeterminate + > + + Your redemption request is being processed. This will take + around 5 hours. + + + )} + )} { flex="1" flexDirection="column" > - {isProcessCompleted ? "total time" : "elapsed time"} - - 15 minutes - + {_isFetching ? ( + + ) : ( + <> + + {isProcessCompleted ? "total time" : "elapsed time"} + + + {`${time.days}d ${time.hours}h ${time.minutes}m`} + - Transacion History - - {transactions - .filter((item) => !!item.txHash) - .map((item) => ( - - - {item.label}{" "} - - . - - - ))} - - {!shouldDisplaySuccessStep && ( - + Transacion History + + {transactions + .filter((item) => !!item.txHash) + .map((item) => ( + + + {item.label}{" "} + + . + + + ))} + + {!shouldDisplaySuccessStep && ( + + )} + )} @@ -250,6 +306,7 @@ const SuccessStep: FC<{ label="Unminted Amount" tokenAmount={unmintedAmount} tokenSymbol="tBTC" + tokenDecimals={8} precision={6} higherPrecision={8} /> @@ -263,6 +320,7 @@ const SuccessStep: FC<{ @@ -275,6 +333,18 @@ const SuccessStep: FC<{ ) } +const AsideSectionSkeleton: FC = () => { + return ( + <> + + + + + + + ) +} + UnmintDetails.route = { path: "redemption/:redemptionKey", index: false, diff --git a/src/pages/tBTC/Bridge/components/BridgeProcessDetailsPageSkeleton.tsx b/src/pages/tBTC/Bridge/components/BridgeProcessDetailsPageSkeleton.tsx new file mode 100644 index 000000000..5c7b75680 --- /dev/null +++ b/src/pages/tBTC/Bridge/components/BridgeProcessDetailsPageSkeleton.tsx @@ -0,0 +1,20 @@ +import { FC } from "react" +import { + SkeletonText, + Skeleton, + SkeletonCircle, +} from "@threshold-network/components" + +export const BridgeProcessDetailsPageSkeleton: FC = () => { + return ( + <> + + + + + + + + + ) +} diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index c634ce3c2..0adc1c4ba 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -17,6 +17,7 @@ import { ZERO, isPublicKeyHashTypeAddress, isSameETHAddress, + AddressZero, isPayToScriptHashTypeAddress, } from "../utils" import { @@ -24,6 +25,7 @@ import { computeHash160, createOutputScriptFromAddress, decodeBitcoinAddress, + Transaction as BitcoinTransaction, TransactionHash, UnspentTransactionOutput, } from "@keep-network/tbtc-v2.ts/dist/src/bitcoin" @@ -42,10 +44,10 @@ import { import TBTCVault from "@keep-network/tbtc-v2/artifacts/TBTCVault.json" import Bridge from "@keep-network/tbtc-v2/artifacts/Bridge.json" import TBTCToken from "@keep-network/tbtc-v2/artifacts/TBTC.json" -import { BigNumber, BigNumberish, Contract } from "ethers" +import { BigNumber, BigNumberish, Contract, utils } from "ethers" import { ContractCall, IMulticall } from "../multicall" -import { Interface } from "ethers/lib/utils" import { BlockTag } from "@ethersproject/abstract-provider" +import { LogDescription } from "ethers/lib/utils" import { requestRedemption, findWalletForRedemption, @@ -65,7 +67,7 @@ export interface BridgeActivity { depositKey: string } -interface RevealedDepositEvent { +export interface RevealedDepositEvent { amount: string walletPublicKeyHash: string fundingTxHash: string @@ -75,6 +77,56 @@ interface RevealedDepositEvent { blockNumber: BlockTag } +export interface RedemptionRequestedEvent { + amount: string + walletPublicKeyHash: string + redeemerOutputScript: string + redeemer: string + treasuryFee: string + txMaxFee: string + blockNumber: BlockTag + txHash: string +} + +type QueryEventFilter = { fromBlock?: BlockTag; toBlock?: BlockTag } + +type RedemptionRequestedEventFilter = { + walletPublicKeyHash?: string | string[] + redeemer?: string | string[] +} & QueryEventFilter + +interface RedemptionRequest { + redeemer: string + requestedAmount: NumberType + treasuryFee: NumberType + txMaxFee: NumberType + requestedAt: number + isPending: boolean + isTimedOut: boolean +} + +type RedemptionTimedOutEventFilter = { + walletPublicKeyHash?: string | string[] +} & QueryEventFilter + +interface RedemptionTimedOutEvent { + walletPublicKeyHash: string + redeemerOutputScript: string + txHash: string + blockNumber: BlockTag +} + +type RedemptionsCompletedEventFilter = { + walletPublicKeyHash: string +} & QueryEventFilter + +interface RedemptionsCompletedEvent { + walletPublicKeyHash: string + redemptionBitcoinTxHash: string + txHash: string + blockNumber: BlockTag +} + type BitcoinTransactionHashByteOrder = "little-endian" | "big-endian" export type RedemptionWalletData = Awaited< @@ -263,6 +315,64 @@ export interface ITBTC { amount: BigNumberish, btcAddress: string ): Promise + + /** + * Builds a redemption key required to refer a redemption request. + * @param walletPublicKeyHash The wallet public key hash that identifies the + * pending redemption (along with the redeemer output script). + * @param redeemerOutputScript The redeemer output script that identifies the + * pending redemption (along with the wallet public key hash). + * @returns The redemption key. + */ + buildRedemptionKey( + walletPublicKeyHash: string, + redeemerOutputScript: string + ): string + + /** + * Gets the full transaction object for given transaction hash. + * @param transactionHash Hash of the transaction. + * @returns Transaction object. + */ + getBitcoinTransaction(transactionHash: string): Promise + + /** + * Gets emitted `RedemptionRequested` events. + * @param filter Filters to find emitted events by indexed params and block + * range. + * @returns Redemption requests filtered by filter params. + */ + getRedemptionRequestedEvents( + filter: RedemptionRequestedEventFilter + ): Promise + + /** + * Gets the redemption details from the on-chain contract by the redemption + * key. It also determines if redemption is pending or timed out. + * @param redemptionKey The redemption key. + * @returns Promise with the redemption details. + */ + getRedemptionRequest(redemptionKey: string): Promise + + /** + * Gets emitted `RedemptionTimedOut` events. + * @param filter Filters to find emitted events by indexed params and block + * range. + * @returns Redemption timed out events filtered by filter params. + */ + getRedemptionTimedOutEvents( + filter: RedemptionTimedOutEventFilter + ): Promise + + /** + * Gets emitted `RedemptionsCompleted` events. + * @param filter Filters to find emitted events by indexed params and block + * range. + * @returns Redemptions completed events filtered by filter params. + */ + getRedemptionsCompletedEvents( + filter: RedemptionsCompletedEventFilter + ): Promise } export class TBTC implements ITBTC { @@ -443,7 +553,7 @@ export class TBTC implements ITBTC { }> => { const calls: ContractCall[] = [ { - interface: new Interface(Bridge.abi), + interface: this._bridgeContract.interface, address: Bridge.address, method: "depositParameters", args: [], @@ -789,4 +899,220 @@ export class TBTC implements ITBTC { satoshis, } } + + buildRedemptionKey = ( + walletPublicKeyHash: string, + redeemerOutputScript: string + ) => { + return utils.solidityKeccak256( + ["bytes32", "bytes20"], + [ + utils.solidityKeccak256(["bytes"], [redeemerOutputScript]), + walletPublicKeyHash, + ] + ) + } + + getBitcoinTransaction = async ( + transacionHash: string + ): Promise => { + return this._bitcoinClient.getTransaction( + TransactionHash.from(transacionHash) + ) + } + + getRedemptionRequestedEvents = async ( + filter: RedemptionRequestedEventFilter + ): Promise => { + const { walletPublicKeyHash, redeemer, fromBlock, toBlock } = filter + + // TODO: Use this sinppet to fetch events once we provide a fix in the `ethers.js` lib. + // const events = await getContractPastEvents(this.bridgeContract, { + // eventName: "RedemptionRequested", + // filterParams: [walletPublicKeyHash, null, redeemer], + // fromBlock, + // toBlock, + // }) + + // This is a workaround to get the `RedemptionRequested` events by + // `walletPublicKeyHash` param. 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` + + const encodeAddress = (address: string) => + utils.hexZeroPad(utils.hexlify(address), 32) + + const filterTopics = [ + utils.id( + "RedemptionRequested(bytes20,bytes,address,uint64,uint64,uint64)" + ), + this._encodeWalletPublicKeyHash(walletPublicKeyHash), + Array.isArray(redeemer) + ? redeemer.map(encodeAddress) + : encodeAddress(redeemer ?? AddressZero), + ] + + const logs = await this.bridgeContract.queryFilter( + { + address: this.bridgeContract.address, + topics: filterTopics, + }, + fromBlock, + toBlock + ) + + return logs + .map((log) => ({ + ...this.bridgeContract.interface.parseLog(log), + blockNumber: log.blockNumber, + transactionHash: log.transactionHash, + })) + .map(this._parseRedemptionRequestedEvent) + } + + private _encodeWalletPublicKeyHash = ( + walletPublicKeyHash?: string | string[] + ): string | string[] => { + const encodeWalletPublicKeyHash = (hash: string) => + utils.defaultAbiCoder.encode(["bytes20"], [hash]) + + return Array.isArray(walletPublicKeyHash) + ? walletPublicKeyHash.map(encodeWalletPublicKeyHash) + : encodeWalletPublicKeyHash(walletPublicKeyHash ?? "0x00") + } + + _parseRedemptionRequestedEvent = ( + event: LogDescription & { + blockNumber: number + transactionHash: string + } + ): RedemptionRequestedEvent => { + return { + amount: event.args?.requestedAmount.toString(), + walletPublicKeyHash: event.args?.walletPubKeyHash.toString(), + redeemerOutputScript: event.args?.redeemerOutputScript.toString(), + redeemer: event.args?.redeemer.toString(), + treasuryFee: event.args?.treasuryFee.toString(), + txMaxFee: event.args?.txMaxFee.toString(), + blockNumber: event.blockNumber, + txHash: event.transactionHash, + } + } + + getRedemptionRequest = async ( + redemptionKey: string + ): Promise => { + const [[pending], [timedOut]] = await this._multicall.aggregate([ + { + interface: this._bridgeContract.interface, + address: this._bridgeContract.address, + method: "pendingRedemptions", + args: [redemptionKey], + }, + { + interface: this._bridgeContract.interface, + address: this._bridgeContract.address, + method: "timedOutRedemptions", + args: [redemptionKey], + }, + ]) + + const isPending = pending.requestedAt !== 0 + const isTimedOut = !isPending ? timedOut.requestedAt !== 0 : false + + let redemptionData: Omit + + if (isPending) { + redemptionData = pending + } else if (isTimedOut) { + redemptionData = timedOut + } else { + redemptionData = { + redeemer: AddressZero, + requestedAmount: ZERO, + treasuryFee: ZERO, + txMaxFee: ZERO, + requestedAt: 0, + } + } + + return { + ...redemptionData, + redeemer: redemptionData.redeemer.toString(), + isPending, + isTimedOut, + } + } + + getRedemptionTimedOutEvents = async ( + filter: RedemptionTimedOutEventFilter + ): Promise => { + const { walletPublicKeyHash, fromBlock, toBlock } = filter + + // TODO: Use `getContractPastEvents` to fetch events once we provide a fix + // in the `ethers.js` lib. This is a workaround to get the + // `RedemptionTimedOut` events by `walletPublicKeyHash` param. The + // `ethers.js` lib encodes the `bytesX` param in the wrong way. + const filterTopics = [ + utils.id("RedemptionTimedOut(bytes20,bytes)"), + this._encodeWalletPublicKeyHash(walletPublicKeyHash), + ] + + const logs = await this.bridgeContract.queryFilter( + { + address: this.bridgeContract.address, + topics: filterTopics, + }, + fromBlock, + toBlock + ) + + return logs.map((log) => ({ + walletPublicKeyHash: log.args?.walletPubKeyHash.toString(), + redeemerOutputScript: log.args?.redeemerOutputScript.toString(), + blockNumber: log.blockNumber, + txHash: log.transactionHash, + })) + } + + getRedemptionsCompletedEvents = async ( + filter: RedemptionsCompletedEventFilter + ): Promise => { + const { walletPublicKeyHash, fromBlock, toBlock } = filter + + // TODO: Use `getContractPastEvents` to fetch events once we provide a fix + // in the `ethers.js` lib. This is a workaround to get the + // `RedemptionsCompleted` events by `walletPublicKeyHash` param. The + // `ethers.js` lib encodes the `bytesX` param in the wrong way. + const filterTopics = [ + utils.id("RedemptionsCompleted(bytes20,bytes32)"), + this._encodeWalletPublicKeyHash(walletPublicKeyHash), + ] + + const logs = await this.bridgeContract.queryFilter( + { + address: this.bridgeContract.address, + topics: filterTopics, + }, + fromBlock, + toBlock + ) + + return logs.map((log) => ({ + walletPublicKeyHash: log.args?.walletPubKeyHash.toString(), + redemptionBitcoinTxHash: Hex.from(log.args?.redemptionTxHash.toString()) + .reverse() + .toString(), + blockNumber: log.blockNumber, + txHash: log.transactionHash, + })) + } } diff --git a/src/threshold-ts/utils/bitcoin.ts b/src/threshold-ts/utils/bitcoin.ts index 5b822cc56..26563fa38 100644 --- a/src/threshold-ts/utils/bitcoin.ts +++ b/src/threshold-ts/utils/bitcoin.ts @@ -40,3 +40,13 @@ export const isPayToScriptHashTypeAddress = (address: string): boolean => { export const reverseTxHash = (txHash: string): TransactionHash => { return TransactionHash.from(txHash).reverse() } + +export const prependScriptPubKeyByLength = (scriptPubKey: string) => { + const rawRedeemerOutputScript = Buffer.from(scriptPubKey.toString(), "hex") + + // Prefix the output script bytes buffer with 0x and its own length. + return `0x${Buffer.concat([ + Buffer.from([rawRedeemerOutputScript.length]), + rawRedeemerOutputScript, + ]).toString("hex")}` +} diff --git a/src/web3/hooks/index.ts b/src/web3/hooks/index.ts index c18ab75d6..d1a1b7293 100644 --- a/src/web3/hooks/index.ts +++ b/src/web3/hooks/index.ts @@ -18,3 +18,4 @@ export * from "./useTStakingContract" export * from "./useKeepTokenStakingContract" export * from "./usePREContract" export * from "./useClaimMerkleRewardsTransaction" +export * from "./useGetBlock" diff --git a/src/web3/hooks/useGetBlock.ts b/src/web3/hooks/useGetBlock.ts new file mode 100644 index 000000000..6406176b4 --- /dev/null +++ b/src/web3/hooks/useGetBlock.ts @@ -0,0 +1,20 @@ +import { useCallback } from "react" +import { BlockTag } from "@ethersproject/abstract-provider" +import { Web3Provider } from "@ethersproject/providers" +import { useThreshold } from "../../contexts/ThresholdContext" +import { getProviderOrSigner } from "../../threshold-ts/utils" + +export const useGetBlock = () => { + const threshold = useThreshold() + + return useCallback( + async (blockTag: BlockTag) => { + const provider = getProviderOrSigner( + threshold.config.ethereum.providerOrSigner as any + ) as Web3Provider + + return provider.getBlock(blockTag) + }, + [threshold] + ) +} diff --git a/yarn.lock b/yarn.lock index af39bdb8c..d75f78be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6673,10 +6673,15 @@ bufio@^1.0.6: resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.2.0.tgz#b9ad1c06b0d9010363c387c39d2810a7086d143f" integrity sha512-UlFk8z/PwdhYQTXSQQagwGAdtRI83gib2n4uy4rQnenxUM2yQi8lBDzF230BNk+3wAoZDxYRoBwVVUPgHa9MCA== -"bufio@git+https://github.com/bcoin-org/bufio.git#semver:~1.0.6", bufio@~1.0.7: +"bufio@git+https://github.com/bcoin-org/bufio.git#semver:~1.0.6": version "1.0.7" resolved "git+https://github.com/bcoin-org/bufio.git#91ae6c93899ff9fad7d7cee9afd2a1c4933ca984" +bufio@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.0.7.tgz#b7f63a1369a0829ed64cc14edf0573b3e382a33e" + integrity sha512-bd1dDQhiC+bEbEfg56IdBv7faWa6OipMs/AFFFvtFnB3wAYjlwQpQRZ0pm6ZkgtfL0pILRXhKxOiQj6UzoMR7A== + builtin-modules@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -12703,10 +12708,15 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -"loady@git+https://github.com/chjj/loady.git#semver:~0.0.1", loady@~0.0.1, loady@~0.0.5: +"loady@git+https://github.com/chjj/loady.git#semver:~0.0.1": version "0.0.5" resolved "git+https://github.com/chjj/loady.git#b94958b7ee061518f4b85ea6da380e7ee93222d5" +loady@~0.0.1, loady@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881" + integrity sha512-uxKD2HIj042/HBx77NBcmEPsD+hxCgAtjEWlYNScuUjIsh/62Uyu39GOR68TBR68v+jqDL9zfftCWoUo4y03sQ== + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"