From b6070673c95d171fe1ae08da77cb12b39168e426 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 13 Jul 2023 11:16:57 +0200 Subject: [PATCH 01/10] Refactor `buildRedemptionDetailsLink` Use builder pattern to build the redemption details link. Also keep the `buildRedemptionDetailsLink` function to not break the current API. We need more generic approach to build link because in `RedemptionRequested` evnet we do not have `btcAddress` but `redeemerOutputScript` in expected format. Also in event we can find `walletPublicKeyHash` not `walletPublicKey` - using builder we can build link from `walletPublicKey` or `walletPublicKeyHash` and `btcAddress` or `redeemerOutpuScript` and the builder class is responsible for converting params to correct format. --- src/utils/tBTC.ts | 86 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/src/utils/tBTC.ts b/src/utils/tBTC.ts index d30bb6720..e63391d57 100644 --- a/src/utils/tBTC.ts +++ b/src/utils/tBTC.ts @@ -1,4 +1,3 @@ -import { To } from "react-router-dom" import { BitcoinNetwork } from "../threshold-ts/types" import { BridgeProcess } from "../types/tbtc" import { @@ -82,24 +81,79 @@ export const getBridgeBTCSupportedAddressPrefixesText = ( export const UNMINT_MIN_AMOUNT = "10000000000000000" // 0.01 +export class RedemptionDetailsLinkBuilder { + private walletPublicKeyHash?: string + private txHash?: string + private redeemer?: string + private redeemerOutputScript?: string + + static createFromTxHash = (txHash: string) => { + const builder = new RedemptionDetailsLinkBuilder() + + return builder.withTxHash(txHash) + } + + withWalletPublicKey = (walletPublicKey: string) => { + this.walletPublicKeyHash = `0x${computeHash160(walletPublicKey)}` + return this + } + + withWalletPublicKeyHash = (walletPublicKeyHash: string) => { + this.walletPublicKeyHash = walletPublicKeyHash + return this + } + + withBitcoinAddress = (btcAddress: string) => { + const redeemerOutputScript = createOutputScriptFromAddress(btcAddress) + this.redeemerOutputScript = prependScriptPubKeyByLength( + redeemerOutputScript.toString() + ) + return this + } + + withRedeemerOutputScript = (redeemerOutputScript: string) => { + this.redeemerOutputScript = redeemerOutputScript + return this + } + + withRedeemer = (redeemer: string) => { + this.redeemer = redeemer + return this + } + + withTxHash = (txHash: string) => { + this.txHash = txHash + return this + } + + build = () => { + if ( + !this.txHash || + !this.walletPublicKeyHash || + !this.redeemerOutputScript || + !this.redeemer + ) { + throw new Error("Required parameters not set") + } + + const queryParams = new URLSearchParams() + queryParams.set("redeemer", this.redeemer) + queryParams.set("walletPublicKeyHash", this.walletPublicKeyHash) + queryParams.set("redeemerOutputScript", this.redeemerOutputScript) + + return `/tBTC/unmint/redemption/${this.txHash}?${queryParams.toString()}` + } +} + export const buildRedemptionDetailsLink = ( txHash: string, redeemer: string, walletPublicKey: string, btcAddress: string -): To => { - const queryParams = new URLSearchParams() - queryParams.set("redeemer", redeemer) - queryParams.set("walletPublicKeyHash", `0x${computeHash160(walletPublicKey)}`) - - const redeemerOutputScript = createOutputScriptFromAddress(btcAddress) - const prefixedRedeemerOutputScript = prependScriptPubKeyByLength( - redeemerOutputScript.toString() - ) - queryParams.set("redeemerOutputScript", prefixedRedeemerOutputScript) - - return { - pathname: `/tBTC/unmint/redemption/${txHash}`, - search: `?${queryParams.toString()}`, - } +): string => { + return RedemptionDetailsLinkBuilder.createFromTxHash(txHash) + .withRedeemer(redeemer) + .withWalletPublicKey(walletPublicKey) + .withBitcoinAddress(btcAddress) + .build() } From c7361a2c8ca547b6b202b69a5fd19760793d7abb Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 13 Jul 2023 12:30:09 +0200 Subject: [PATCH 02/10] Update the `bridgeActivity` method in TBTC impl Take into account requested and completed redemptions to display them in the `my activity` table on the bridge page. --- src/components/tBTC/BridgeActivity.tsx | 32 +++- .../tbtc/useSubsribeToDepositRevealedEvent.ts | 1 + src/store/tbtc/tbtcSlice.ts | 14 +- src/threshold-ts/tbtc/index.ts | 151 ++++++++++++++++-- src/types/tbtc.ts | 4 +- 5 files changed, 184 insertions(+), 18 deletions(-) diff --git a/src/components/tBTC/BridgeActivity.tsx b/src/components/tBTC/BridgeActivity.tsx index a999c19ef..ab7f42c05 100644 --- a/src/components/tBTC/BridgeActivity.tsx +++ b/src/components/tBTC/BridgeActivity.tsx @@ -1,4 +1,5 @@ import { FC, createContext, useContext, ReactElement } from "react" +import { useWeb3React } from "@web3-react/core" import { Badge, BodyMd, @@ -18,12 +19,14 @@ import { import { BridgeActivityStatus, BridgeActivity as BridgeActivityType, + UnminBridgeActivityAdditionalData, } from "../../threshold-ts/tbtc" import emptyHistoryImageSrcDark from "../../static/images/tBTC-bridge-no-history-dark.svg" import emptyHistoryImageSrcLight from "../../static/images/tBTC-bridge-no-history-light.svg" import { InlineTokenBalance } from "../TokenBalance" import Link from "../Link" import { OutlineListItem } from "../OutlineListItem" +import { RedemptionDetailsLinkBuilder } from "../../utils/tBTC" export type BridgeActivityProps = { data: BridgeActivityType[] @@ -97,8 +100,28 @@ export const BridgeActivityData: FC = (props) => { const ActivityItem: FC = ({ amount, status, - depositKey, + activityKey, + bridgeProcess, + additionalData, + txHash, }) => { + const { account } = useWeb3React() + + const link = + bridgeProcess === "unmint" + ? RedemptionDetailsLinkBuilder.createFromTxHash(txHash) + .withRedeemer(account!) + .withRedeemerOutputScript( + (additionalData as UnminBridgeActivityAdditionalData) + .redeemerOutputScript + ) + .withWalletPublicKeyHash( + (additionalData as UnminBridgeActivityAdditionalData) + .walletPublicKeyHash + ) + .build() + : `/tBTC/mint/deposit/${activityKey}` + return ( = ({ textDecoration="none" _hover={{ textDecoration: "none" }} color="inherit" - to={`/tBTC/mint/deposit/${depositKey}`} + to={link} > @@ -116,7 +139,7 @@ const ActivityItem: FC = ({ } const renderActivityItem = (item: BridgeActivityType) => ( - + ) const bridgeActivityStatusToBadgeProps: Record< @@ -126,6 +149,9 @@ const bridgeActivityStatusToBadgeProps: Record< [BridgeActivityStatus.MINTED]: { colorScheme: "green", }, + [BridgeActivityStatus.UNMINTED]: { + colorScheme: "green", + }, [BridgeActivityStatus.PENDING]: { colorScheme: "yellow", }, diff --git a/src/hooks/tbtc/useSubsribeToDepositRevealedEvent.ts b/src/hooks/tbtc/useSubsribeToDepositRevealedEvent.ts index 32f592723..4c7aa1981 100644 --- a/src/hooks/tbtc/useSubsribeToDepositRevealedEvent.ts +++ b/src/hooks/tbtc/useSubsribeToDepositRevealedEvent.ts @@ -61,6 +61,7 @@ export const useSubscribeToDepositRevealedEvent = () => { txHash: event.transactionHash, depositor: depositor, depositKey: depositKeyFromEvent, + blockNumber: event.blockNumber, }) ) }, diff --git a/src/store/tbtc/tbtcSlice.ts b/src/store/tbtc/tbtcSlice.ts index 0fafce581..2abc1604e 100644 --- a/src/store/tbtc/tbtcSlice.ts +++ b/src/store/tbtc/tbtcSlice.ts @@ -54,9 +54,10 @@ export const tbtcSlice = createSlice({ amount: string depositor: string txHash: string + blockNumber: number }> ) => { - const { amount, txHash, depositKey } = action.payload + const { amount, txHash, depositKey, blockNumber } = action.payload const history = state.bridgeActivity.data const { itemToUpdate } = findActivityByDepositKey(history, depositKey) @@ -66,7 +67,14 @@ export const tbtcSlice = createSlice({ // Add item only if there is no item with the same deposit key. state.bridgeActivity.data = [ - { amount, txHash, status: BridgeActivityStatus.PENDING, depositKey }, + { + amount, + txHash, + status: BridgeActivityStatus.PENDING, + activityKey: depositKey, + bridgeProcess: "mint", + blockNumber, + }, ...state.bridgeActivity.data, ] }, @@ -116,7 +124,7 @@ function findActivityByDepositKey( depositKey: string ) { const activityIndexItemToUpdate = bridgeActivities.findIndex( - (item) => item.depositKey === depositKey + (item) => item.activityKey === depositKey ) if (activityIndexItemToUpdate < 0) return { index: -1, itemToUpdate: null } diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 0adc1c4ba..004b7d64c 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -58,13 +58,24 @@ export enum BridgeActivityStatus { PENDING = "PENDING", MINTED = "MINTED", ERROR = "ERROR", + UNMINTED = "UNMINTED", } +export type BridgeProcess = "mint" | "unmint" + export interface BridgeActivity { + bridgeProcess: BridgeProcess status: BridgeActivityStatus txHash: string amount: string - depositKey: string + activityKey: string + additionalData?: unknown + blockNumber: number +} + +export interface UnminBridgeActivityAdditionalData { + redeemerOutputScript: string + walletPublicKeyHash: string } export interface RevealedDepositEvent { @@ -74,7 +85,7 @@ export interface RevealedDepositEvent { fundingOutputIndex: string depositKey: string txHash: string - blockNumber: BlockTag + blockNumber: number } export interface RedemptionRequestedEvent { @@ -84,7 +95,7 @@ export interface RedemptionRequestedEvent { redeemer: string treasuryFee: string txMaxFee: string - blockNumber: BlockTag + blockNumber: number txHash: string } @@ -113,7 +124,7 @@ interface RedemptionTimedOutEvent { walletPublicKeyHash: string redeemerOutputScript: string txHash: string - blockNumber: BlockTag + blockNumber: number } type RedemptionsCompletedEventFilter = { @@ -124,7 +135,7 @@ interface RedemptionsCompletedEvent { walletPublicKeyHash: string redemptionBitcoinTxHash: string txHash: string - blockNumber: BlockTag + blockNumber: number } type BitcoinTransactionHashByteOrder = "little-endian" | "big-endian" @@ -258,7 +269,7 @@ export interface ITBTC { /** * Returns the bridge transaction history by depositor in order from the - * newest revealed deposit to the oldest. + * newest activities to the oldest. * @param depositor Depositor Ethereum address. * @returns Bridge transaction history @see {@link BridgeActivity}. */ @@ -640,6 +651,19 @@ export class TBTC implements ITBTC { } bridgeActivity = async (depositor: string): Promise => { + const depositActivities = await this._findDepositActivities(depositor) + + const redemptionActivities: BridgeActivity[] = + await this._findRedemptionActivities(depositor) + + return depositActivities + .concat(redemptionActivities) + .sort((a, b) => a.blockNumber - b.blockNumber) + } + + private _findDepositActivities = async ( + depositor: string + ): Promise => { // We can assume that all revealed deposits have `PENDING` status. const revealedDeposits = await this.findAllRevealedDeposits(depositor) const depositKeys = revealedDeposits.map((_) => _.depositKey) @@ -674,7 +698,7 @@ export class TBTC implements ITBTC { ) return revealedDeposits.map((deposit) => { - const { depositKey, txHash: depositTxHash } = deposit + const { depositKey, txHash: depositTxHash, blockNumber } = deposit let status = BridgeActivityStatus.PENDING let txHash = depositTxHash let amount = estimatedAmountToMintByDepositKey.get(depositKey) ?? ZERO @@ -688,7 +712,14 @@ export class TBTC implements ITBTC { txHash = cancelledDeposits.get(depositKey)! } - return { amount: amount.toString(), txHash, status, depositKey } + return { + amount: amount.toString(), + txHash, + status, + activityKey: depositKey, + bridgeProcess: "mint", + blockNumber, + } }) } @@ -802,6 +833,101 @@ export class TBTC implements ITBTC { }) } + private _findRedemptionActivities = async ( + redeemer: string + ): Promise => { + const requestedRedemptions = await this.getRedemptionRequestedEvents({ + redeemer, + }) + + const redemptionsMap = new Map< + string, + { + redemptionKey: string + walletPublicKeyHash: string + txHash: string + blockNumber: number + redeemerOutputScript: string + isPending: boolean + isTimedOut: boolean + requestedAmount: string + requestedAt: number + } + >() + + for (const event of requestedRedemptions) { + const { timestamp: eventTimestamp } = + await this._bridgeContract.provider.getBlock(event.blockNumber) + const redemptionKey = this.buildRedemptionKey( + event.walletPublicKeyHash, + event.redeemerOutputScript + ) + + const redemptionDetails = await this.getRedemptionRequest(redemptionKey) + // 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 this case we want to mark the first redemption as completed and the + // second one as pending. If we do not compare the timestamps both requests + // will be considered as pending. + const isTheSameRedemption = + eventTimestamp === redemptionDetails.requestedAt + + redemptionsMap.set(`${redemptionKey}-${event.txHash}`, { + isPending: isTheSameRedemption && redemptionDetails.isPending, + isTimedOut: isTheSameRedemption && redemptionDetails.isTimedOut, + // We need to get an amount from an event because if the redemption was + // handled sucesfully the `getRedemptionRequest` returns `0`. The + // `amount` in event is in satoshi, so here we convert to token + // precision. + requestedAmount: this._satoshiMultiplier.mul(event.amount).toString(), + requestedAt: isTheSameRedemption ? redemptionDetails.requestedAt : 0, + walletPublicKeyHash: event.walletPublicKeyHash, + redeemerOutputScript: event.redeemerOutputScript, + txHash: event.txHash, + blockNumber: event.blockNumber, + redemptionKey, + }) + } + + const redemptionsMapEntries = Array.from(redemptionsMap.entries()) + + const pendingRedemptions = redemptionsMapEntries + .filter(([, { isPending }]) => isPending) + .map(([, data]) => ({ ...data, status: BridgeActivityStatus.PENDING })) + + const timedOutRedemptions = redemptionsMapEntries + .filter(([, { isTimedOut }]) => isTimedOut) + .map(([, data]) => ({ ...data, status: BridgeActivityStatus.ERROR })) + + const completedRedemptions = redemptionsMapEntries + .filter(([, { requestedAt }]) => requestedAt === 0) + .map(([, data]) => ({ ...data, status: BridgeActivityStatus.UNMINTED })) + + return pendingRedemptions + .concat(timedOutRedemptions) + .concat(completedRedemptions) + .map((data) => ({ + status: data.status, + txHash: data.txHash, + amount: data.requestedAmount, + activityKey: data.redemptionKey, + bridgeProcess: "unmint", + blockNumber: data.blockNumber, + additionalData: { + redeemerOutputScript: data.redeemerOutputScript, + walletPublicKeyHash: data.walletPublicKeyHash, + } as UnminBridgeActivityAdditionalData, + })) + } + buildDepositKey = ( depositTxHash: string, depositOutputIndex: number, @@ -963,6 +1089,7 @@ export class TBTC implements ITBTC { const logs = await this.bridgeContract.queryFilter( { address: this.bridgeContract.address, + // @ts-ignore topics: filterTopics, }, fromBlock, @@ -980,13 +1107,15 @@ export class TBTC implements ITBTC { private _encodeWalletPublicKeyHash = ( walletPublicKeyHash?: string | string[] - ): string | string[] => { + ): string | undefined | string[] => { const encodeWalletPublicKeyHash = (hash: string) => utils.defaultAbiCoder.encode(["bytes20"], [hash]) return Array.isArray(walletPublicKeyHash) ? walletPublicKeyHash.map(encodeWalletPublicKeyHash) - : encodeWalletPublicKeyHash(walletPublicKeyHash ?? "0x00") + : walletPublicKeyHash + ? encodeWalletPublicKeyHash(walletPublicKeyHash) + : undefined } _parseRedemptionRequestedEvent = ( @@ -1069,6 +1198,7 @@ export class TBTC implements ITBTC { const logs = await this.bridgeContract.queryFilter( { address: this.bridgeContract.address, + // @ts-ignore topics: filterTopics, }, fromBlock, @@ -1100,6 +1230,7 @@ export class TBTC implements ITBTC { const logs = await this.bridgeContract.queryFilter( { address: this.bridgeContract.address, + // @ts-ignore topics: filterTopics, }, fromBlock, diff --git a/src/types/tbtc.ts b/src/types/tbtc.ts index 1aa4846fb..14b2d5439 100644 --- a/src/types/tbtc.ts +++ b/src/types/tbtc.ts @@ -1,7 +1,7 @@ import { UnspentTransactionOutput } from "@keep-network/tbtc-v2.ts/dist/src/bitcoin" import { UpdateStateActionPayload } from "./state" import { FetchingState } from "." -import { BridgeActivity } from "../threshold-ts/tbtc" +import { BridgeActivity, BridgeProcess } from "../threshold-ts/tbtc" export type { UnspentTransactionOutputPlainObject } from "../threshold-ts/types" export interface TbtcState { @@ -61,4 +61,4 @@ export type ExternalPoolData = { tvl: number } -export type BridgeProcess = "mint" | "unmint" +export { type BridgeProcess } From eff211e25086738c600bdaddfd4c0e13b15b0165 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 13 Jul 2023 13:03:34 +0200 Subject: [PATCH 03/10] Subscribe to the `RedemptionRequested` event And update the store data based on emitted data. --- src/App.tsx | 2 + src/hooks/tbtc/index.ts | 1 + .../useSubscribeToRedemptionRequestedEvent.ts | 50 +++++++++++++++ src/store/tbtc/tbtcSlice.ts | 64 ++++++++++++++++++- 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts diff --git a/src/App.tsx b/src/App.tsx index 95ceb0e6c..10bad3f3b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -55,6 +55,7 @@ import { useSubscribeToDepositRevealedEvent } from "./hooks/tbtc/useSubsribeToDe import { useSubscribeToOptimisticMintingFinalizedEvent, useSubscribeToOptimisticMintingRequestedEvent, + useSubscribeToRedemptionRequestedEvent, } from "./hooks/tbtc" import { useSentry } from "./hooks/sentry" @@ -80,6 +81,7 @@ const Web3EventHandlerComponent = () => { useSubscribeToDepositRevealedEvent() useSubscribeToOptimisticMintingFinalizedEvent() useSubscribeToOptimisticMintingRequestedEvent() + useSubscribeToRedemptionRequestedEvent() return <> } diff --git a/src/hooks/tbtc/index.ts b/src/hooks/tbtc/index.ts index 5f25dd9f9..cfbd45aa6 100644 --- a/src/hooks/tbtc/index.ts +++ b/src/hooks/tbtc/index.ts @@ -8,3 +8,4 @@ export * from "./useFetchDepositDetails" export * from "./useFetchRecentDeposits" export * from "./useFetchTBTCMetrics" export * from "./useRequestRedemption" +export * from "./useSubscribeToRedemptionRequestedEvent" diff --git a/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts b/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts new file mode 100644 index 000000000..f8caf2a4b --- /dev/null +++ b/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts @@ -0,0 +1,50 @@ +import { useWeb3React } from "@web3-react/core" +import { useSubscribeToContractEvent } from "../../web3/hooks" +import { isSameETHAddress } from "../../web3/utils" +import { useAppDispatch } from "../store" +import { useBridgeContract } from "./useBridgeContract" +import { tbtcSlice } from "../../store/tbtc" +import { BigNumber, Event } from "ethers" +import { useThreshold } from "../../contexts/ThresholdContext" + +export const useSubscribeToRedemptionRequestedEvent = () => { + const contract = useBridgeContract() + const dispatch = useAppDispatch() + const { account } = useWeb3React() + const threshold = useThreshold() + + useSubscribeToContractEvent( + contract, + "RedemptionRequested", + //@ts-ignore + async ( + walletPublicKeyHash: string, + redeemerOutputScript: string, + redeemer: string, + requestedAmount: BigNumber, + treasuryFee: BigNumber, + txMaxFee: BigNumber, + event: Event + ) => { + if (!account || !isSameETHAddress(redeemer, account)) return + + const redemptionKey = threshold.tbtc.buildRedemptionKey( + walletPublicKeyHash, + redeemerOutputScript + ) + + dispatch( + tbtcSlice.actions.redemptionRequested({ + // TODO: Take into account fees, see + // https://github.com/threshold-network/token-dashboard/pull/569 + amount: requestedAmount.mul(BigNumber.from(10).pow(10)).toString(), + txHash: event.transactionHash, + redemptionKey, + blockNumber: event.blockNumber, + additionalData: { redeemerOutputScript, walletPublicKeyHash }, + }) + ) + }, + [null, null, account] + ) +} diff --git a/src/store/tbtc/tbtcSlice.ts b/src/store/tbtc/tbtcSlice.ts index 2abc1604e..df36a35d4 100644 --- a/src/store/tbtc/tbtcSlice.ts +++ b/src/store/tbtc/tbtcSlice.ts @@ -2,7 +2,11 @@ import { createSlice } from "@reduxjs/toolkit" import { PayloadAction } from "@reduxjs/toolkit/dist/createAction" import { MintingStep, TbtcStateKey, TbtcState } from "../../types/tbtc" import { UpdateStateActionPayload } from "../../types/state" -import { BridgeActivityStatus, BridgeActivity } from "../../threshold-ts/tbtc" +import { + BridgeActivityStatus, + BridgeActivity, + UnminBridgeActivityAdditionalData, +} from "../../threshold-ts/tbtc" import { featureFlags } from "../../constants" import { startAppListening } from "../listener" import { @@ -116,6 +120,45 @@ export const tbtcSlice = createSlice({ } }> ) => {}, + redemptionRequested: ( + state, + action: PayloadAction<{ + redemptionKey: string + blockNumber: number + amount: string + txHash: string + additionalData: UnminBridgeActivityAdditionalData + }> + ) => { + const { + payload: { amount, redemptionKey, blockNumber, txHash, additionalData }, + } = action + + const { itemToUpdate } = findRedemptionActivity( + state.bridgeActivity.data, + redemptionKey, + txHash + ) + + // Do not update an array if there is already an item with the same + // redemption key and transaction hash- just in case duplicated Ethereum + // events. + if (itemToUpdate) return + + // Add item only if there is no item with the same deposit key. + state.bridgeActivity.data = [ + { + amount, + txHash, + status: BridgeActivityStatus.PENDING, + activityKey: redemptionKey, + bridgeProcess: "unmint", + blockNumber, + additionalData, + }, + ...state.bridgeActivity.data, + ] + }, }, }) @@ -137,6 +180,25 @@ function findActivityByDepositKey( } } +function findRedemptionActivity( + bridgeActivities: BridgeActivity[], + redemptionKey: string, + txHash: string +) { + const activityIndexItemToUpdate = bridgeActivities.findIndex( + (item) => item.activityKey === redemptionKey && item.txHash === txHash + ) + + if (activityIndexItemToUpdate < 0) return { index: -1, itemToUpdate: null } + + const activityItemToUpdate = bridgeActivities[activityIndexItemToUpdate] + + return { + index: activityIndexItemToUpdate, + itemToUpdate: activityItemToUpdate, + } +} + export const { updateState } = tbtcSlice.actions export const registerTBTCListeners = () => { From 4f374ec7539094177cb75c827123b5b2ac8b3e48 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 13 Jul 2023 13:06:16 +0200 Subject: [PATCH 04/10] Fix bridge activities sorting We should sort from newest to oldest. --- src/threshold-ts/tbtc/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 004b7d64c..56b661fd8 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -658,7 +658,7 @@ export class TBTC implements ITBTC { return depositActivities .concat(redemptionActivities) - .sort((a, b) => a.blockNumber - b.blockNumber) + .sort((a, b) => b.blockNumber - a.blockNumber) } private _findDepositActivities = async ( From fb4038f5f6c48df9be79bcc55c89d4c7abe3295f Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 14 Jul 2023 10:56:30 +0200 Subject: [PATCH 05/10] Improve params validation link builder List missed params in error message. --- src/utils/tBTC.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/utils/tBTC.ts b/src/utils/tBTC.ts index e63391d57..8ff6f5a1a 100644 --- a/src/utils/tBTC.ts +++ b/src/utils/tBTC.ts @@ -127,13 +127,26 @@ export class RedemptionDetailsLinkBuilder { } build = () => { + const params = [ + { label: "transaction hash", value: this.txHash }, + { label: "wallet public key hash", value: this.walletPublicKeyHash }, + { label: "redeemer output script", value: this.redeemerOutputScript }, + { label: "redeemer", value: this.redeemer }, + ] + if ( !this.txHash || !this.walletPublicKeyHash || !this.redeemerOutputScript || !this.redeemer ) { - throw new Error("Required parameters not set") + const missingParams = params.filter((_) => !_.value) + + throw new Error( + `Required parameters not set. Set ${missingParams + .map((_) => _.label) + .join(", ")}.` + ) } const queryParams = new URLSearchParams() From fd6839a2fe8bb56c0153af394a94a377bb3a3269 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 14 Jul 2023 11:04:38 +0200 Subject: [PATCH 06/10] Rename param in `bridgeActivity` fn Since we now gather both deposit and remption data it seems that `depositor` name for the `bridgeAcivity` parameter doesn't really match and might be misleading. Here we rename param to just `account`. --- src/threshold-ts/tbtc/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 56b661fd8..a7e3ef09d 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -270,10 +270,10 @@ export interface ITBTC { /** * Returns the bridge transaction history by depositor in order from the * newest activities to the oldest. - * @param depositor Depositor Ethereum address. + * @param account Ethereum address. * @returns Bridge transaction history @see {@link BridgeActivity}. */ - bridgeActivity(depositor: string): Promise + bridgeActivity(account: string): Promise /** * Builds the deposit key required to refer a revealed deposit. @@ -650,11 +650,11 @@ export class TBTC implements ITBTC { return 6 } - bridgeActivity = async (depositor: string): Promise => { - const depositActivities = await this._findDepositActivities(depositor) + bridgeActivity = async (account: string): Promise => { + const depositActivities = await this._findDepositActivities(account) const redemptionActivities: BridgeActivity[] = - await this._findRedemptionActivities(depositor) + await this._findRedemptionActivities(account) return depositActivities .concat(redemptionActivities) From fecc080b8cef6af369159f6b865f7d844273ed76 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 14 Jul 2023 11:32:05 +0200 Subject: [PATCH 07/10] Simplify fn that fetches the redemptions Use array instead of map. While working on this, I thought getting redemption by key would be useful, but it looks like it's unnecessary now. --- src/threshold-ts/tbtc/index.ts | 73 ++++++++++------------------------ 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index a7e3ef09d..6d9e96ab0 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -840,20 +840,7 @@ export class TBTC implements ITBTC { redeemer, }) - const redemptionsMap = new Map< - string, - { - redemptionKey: string - walletPublicKeyHash: string - txHash: string - blockNumber: number - redeemerOutputScript: string - isPending: boolean - isTimedOut: boolean - requestedAmount: string - requestedAt: number - } - >() + const redemptions: BridgeActivity[] = [] for (const event of requestedRedemptions) { const { timestamp: eventTimestamp } = @@ -880,52 +867,34 @@ export class TBTC implements ITBTC { const isTheSameRedemption = eventTimestamp === redemptionDetails.requestedAt - redemptionsMap.set(`${redemptionKey}-${event.txHash}`, { - isPending: isTheSameRedemption && redemptionDetails.isPending, - isTimedOut: isTheSameRedemption && redemptionDetails.isTimedOut, + const isTimedOut = isTheSameRedemption && redemptionDetails.isTimedOut + const requestedAt = isTheSameRedemption + ? redemptionDetails.requestedAt + : 0 + + let status = BridgeActivityStatus.PENDING + if (isTimedOut) status = BridgeActivityStatus.ERROR + if (requestedAt === 0) status = BridgeActivityStatus.UNMINTED + + redemptions.push({ + status, + txHash: event.txHash, // We need to get an amount from an event because if the redemption was // handled sucesfully the `getRedemptionRequest` returns `0`. The // `amount` in event is in satoshi, so here we convert to token // precision. - requestedAmount: this._satoshiMultiplier.mul(event.amount).toString(), - requestedAt: isTheSameRedemption ? redemptionDetails.requestedAt : 0, - walletPublicKeyHash: event.walletPublicKeyHash, - redeemerOutputScript: event.redeemerOutputScript, - txHash: event.txHash, + amount: this._satoshiMultiplier.mul(event.amount).toString(), + activityKey: redemptionKey, + bridgeProcess: "unmint", blockNumber: event.blockNumber, - redemptionKey, + additionalData: { + redeemerOutputScript: event.redeemerOutputScript, + walletPublicKeyHash: event.walletPublicKeyHash, + } as UnminBridgeActivityAdditionalData, }) } - const redemptionsMapEntries = Array.from(redemptionsMap.entries()) - - const pendingRedemptions = redemptionsMapEntries - .filter(([, { isPending }]) => isPending) - .map(([, data]) => ({ ...data, status: BridgeActivityStatus.PENDING })) - - const timedOutRedemptions = redemptionsMapEntries - .filter(([, { isTimedOut }]) => isTimedOut) - .map(([, data]) => ({ ...data, status: BridgeActivityStatus.ERROR })) - - const completedRedemptions = redemptionsMapEntries - .filter(([, { requestedAt }]) => requestedAt === 0) - .map(([, data]) => ({ ...data, status: BridgeActivityStatus.UNMINTED })) - - return pendingRedemptions - .concat(timedOutRedemptions) - .concat(completedRedemptions) - .map((data) => ({ - status: data.status, - txHash: data.txHash, - amount: data.requestedAmount, - activityKey: data.redemptionKey, - bridgeProcess: "unmint", - blockNumber: data.blockNumber, - additionalData: { - redeemerOutputScript: data.redeemerOutputScript, - walletPublicKeyHash: data.walletPublicKeyHash, - } as UnminBridgeActivityAdditionalData, - })) + return redemptions } buildDepositKey = ( From fe8b96e24e013c4e86e8faff74f4bc056fd3e0b4 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 14 Jul 2023 12:06:08 +0200 Subject: [PATCH 08/10] Add utils fn Add utils functions that help convert number to token precision. --- .../tbtc/useSubscribeToRedemptionRequestedEvent.ts | 3 ++- src/threshold-ts/tbtc/index.ts | 3 ++- src/threshold-ts/utils/bitcoin.ts | 2 ++ src/threshold-ts/utils/chain.ts | 10 ++++++++++ src/threshold-ts/utils/math.ts | 9 +++++++++ 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts b/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts index f8caf2a4b..1a6bb7a1f 100644 --- a/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts +++ b/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts @@ -6,6 +6,7 @@ import { useBridgeContract } from "./useBridgeContract" import { tbtcSlice } from "../../store/tbtc" import { BigNumber, Event } from "ethers" import { useThreshold } from "../../contexts/ThresholdContext" +import { fromSatoshiToTokenPrecision } from "../../threshold-ts/utils" export const useSubscribeToRedemptionRequestedEvent = () => { const contract = useBridgeContract() @@ -37,7 +38,7 @@ export const useSubscribeToRedemptionRequestedEvent = () => { tbtcSlice.actions.redemptionRequested({ // TODO: Take into account fees, see // https://github.com/threshold-network/token-dashboard/pull/569 - amount: requestedAmount.mul(BigNumber.from(10).pow(10)).toString(), + amount: fromSatoshiToTokenPrecision(requestedAmount).toString(), txHash: event.transactionHash, redemptionKey, blockNumber: event.blockNumber, diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 6d9e96ab0..4ad7e6566 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -19,6 +19,7 @@ import { isSameETHAddress, AddressZero, isPayToScriptHashTypeAddress, + fromSatoshiToTokenPrecision, } from "../utils" import { Client, @@ -883,7 +884,7 @@ export class TBTC implements ITBTC { // handled sucesfully the `getRedemptionRequest` returns `0`. The // `amount` in event is in satoshi, so here we convert to token // precision. - amount: this._satoshiMultiplier.mul(event.amount).toString(), + amount: fromSatoshiToTokenPrecision(event.amount).toString(), activityKey: redemptionKey, bridgeProcess: "unmint", blockNumber: event.blockNumber, diff --git a/src/threshold-ts/utils/bitcoin.ts b/src/threshold-ts/utils/bitcoin.ts index e25e11cdd..297aab4a0 100644 --- a/src/threshold-ts/utils/bitcoin.ts +++ b/src/threshold-ts/utils/bitcoin.ts @@ -12,6 +12,8 @@ import { validate, } from "bitcoin-address-validation" +export const BITCOIN_PRECISION = 8 + export const isValidBtcAddress = ( address: string, network: BitcoinNetwork = BitcoinNetwork.Mainnet diff --git a/src/threshold-ts/utils/chain.ts b/src/threshold-ts/utils/chain.ts index 674299d2e..d154c55d1 100644 --- a/src/threshold-ts/utils/chain.ts +++ b/src/threshold-ts/utils/chain.ts @@ -1,4 +1,7 @@ +import { BigNumberish } from "ethers" import { defaultAbiCoder } from "ethers/lib/utils" +import { BITCOIN_PRECISION } from "./bitcoin" +import { to1ePrecision } from "./math" export const isValidType = (paramType: string, value: string) => { try { @@ -8,3 +11,10 @@ export const isValidType = (paramType: string, value: string) => { return false } } + +export const fromSatoshiToTokenPrecision = ( + value: BigNumberish, + tokenPrecision: number = 18 +) => { + return to1ePrecision(value, tokenPrecision - BITCOIN_PRECISION) +} diff --git a/src/threshold-ts/utils/math.ts b/src/threshold-ts/utils/math.ts index 4fa9ef151..12567342c 100644 --- a/src/threshold-ts/utils/math.ts +++ b/src/threshold-ts/utils/math.ts @@ -18,3 +18,12 @@ export const min = (a: BigNumberish, b: BigNumberish) => { export const max = (a: BigNumberish, b: BigNumberish) => { return compare(a, b, "gt") } + +export function to1ePrecision(n: BigNumberish, precision: number): BigNumber { + const decimalMultiplier = BigNumber.from(10).pow(precision) + return BigNumber.from(n).mul(decimalMultiplier) +} + +export function to1e18(n: BigNumberish): BigNumber { + return to1ePrecision(n, 18) +} From 865587e52075b80cc2c84240f98f99b00d4a3d91 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 14 Jul 2023 14:22:25 +0200 Subject: [PATCH 09/10] Add comment in `BridgeActivity` interface Point out that `activityKey` stores the redemption key for redemption and deposit key for deposit. --- src/threshold-ts/tbtc/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 4ad7e6566..b47573d9c 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -69,6 +69,9 @@ export interface BridgeActivity { status: BridgeActivityStatus txHash: string amount: string + /** + * Stores the deposit key for deposit and redemption key for redemption. + */ activityKey: string additionalData?: unknown blockNumber: number From 434e5585cce7f039be27432ff821d9a8cf7e806c Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 14 Jul 2023 14:47:58 +0200 Subject: [PATCH 10/10] Remove TODO There is no need to take into account fees in this case because in the `my activity` table we want to display the tBTC amount that user requested to unmint - they actually decrease their balance of tBTC token by this amount. --- src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts b/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts index 1a6bb7a1f..02aa68566 100644 --- a/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts +++ b/src/hooks/tbtc/useSubscribeToRedemptionRequestedEvent.ts @@ -36,8 +36,6 @@ export const useSubscribeToRedemptionRequestedEvent = () => { dispatch( tbtcSlice.actions.redemptionRequested({ - // TODO: Take into account fees, see - // https://github.com/threshold-network/token-dashboard/pull/569 amount: fromSatoshiToTokenPrecision(requestedAmount).toString(), txHash: event.transactionHash, redemptionKey,