From df6dc150c0fd3f6e4e7639780fa922dd3e60ccba Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 09:58:11 +0200 Subject: [PATCH 01/17] Create a hook that fetches block data --- src/web3/hooks/index.ts | 1 + src/web3/hooks/useGetBlock.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/web3/hooks/useGetBlock.ts 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] + ) +} From 4abb335dc62e3e5cf1404b158455ba6b0a503623 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 10:12:21 +0200 Subject: [PATCH 02/17] Bump tbtc-v2.ts lib --- yarn.lock | 94 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5efbbbcdf..66b78e9a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3083,6 +3083,17 @@ "@openzeppelin/contracts-upgradeable" "^4.6.0" "@threshold-network/solidity-contracts" "1.3.0-dev.5" +"@keep-network/ecdsa@2.1.0-dev.14": + version "2.1.0-dev.14" + resolved "https://registry.npmjs.org/@keep-network/ecdsa/-/ecdsa-2.1.0-dev.14.tgz#1391174fddb6d13bab8309b482b6cdea0cd1ef84" + integrity sha512-kvUIws/XPr1MHTEXByR+lfqPfj8Mg0V9dEnsm/cMO7N7UaB55eBF+LDEVTDgDlmaYFJfr/iC3v6EkPyz9BLNtQ== + dependencies: + "@keep-network/random-beacon" "2.1.0-dev.14" + "@keep-network/sortition-pools" "^2.0.0-pre.16" + "@openzeppelin/contracts" "^4.6.0" + "@openzeppelin/contracts-upgradeable" "^4.6.0" + "@threshold-network/solidity-contracts" "1.3.0-dev.5" + "@keep-network/keep-core@1.8.0-dev.5": version "1.8.0-dev.5" resolved "https://registry.yarnpkg.com/@keep-network/keep-core/-/keep-core-1.8.0-dev.5.tgz#8b4d08ec437f29c94723ee54fcf76456ba5408c3" @@ -3145,6 +3156,26 @@ "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" "@threshold-network/solidity-contracts" "1.3.0-dev.5" +"@keep-network/random-beacon@2.1.0-dev.14": + version "2.1.0-dev.14" + resolved "https://registry.npmjs.org/@keep-network/random-beacon/-/random-beacon-2.1.0-dev.14.tgz#d9fac9fa8a5a06ea0985114c4ca79e4805c16d55" + integrity sha512-FdVSW2VtUIcwPCrnrWUudbXOFi+SKZ6cEz7P3+gO+49DFas4ApH6lkRILD/DUHQDMV7D56TxAdw/DHt0dbA+wg== + dependencies: + "@keep-network/sortition-pools" "^2.0.0-pre.16" + "@openzeppelin/contracts" "4.7.3" + "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" + "@threshold-network/solidity-contracts" "1.3.0-dev.5" + +"@keep-network/random-beacon@2.1.0-dev.15": + version "2.1.0-dev.15" + resolved "https://registry.npmjs.org/@keep-network/random-beacon/-/random-beacon-2.1.0-dev.15.tgz#541620c469e3bc75a5d1f7649889540b0e032e9e" + integrity sha512-vxBICRtmqSmJtFU5hZMpwB0alhgKchyMbxk4DtLZ7T2zBjd5tjt3CqeKEk+ON09g7yL1mIxY07InP4okviUK4A== + dependencies: + "@keep-network/sortition-pools" "^2.0.0-pre.16" + "@openzeppelin/contracts" "4.7.3" + "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" + "@threshold-network/solidity-contracts" "1.3.0-dev.5" + "@keep-network/sortition-pools@1.2.0-dev.1": version "1.2.0-dev.1" resolved "https://registry.yarnpkg.com/@keep-network/sortition-pools/-/sortition-pools-1.2.0-dev.1.tgz#2ee371f1dd1ff71f6d05c9ddc2a83a4a93ff56b3" @@ -3161,12 +3192,12 @@ "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" "@keep-network/tbtc-v2.ts@development": - version "1.3.0-dev.2" - resolved "https://registry.yarnpkg.com/@keep-network/tbtc-v2.ts/-/tbtc-v2.ts-1.3.0-dev.2.tgz#59c9cc83db09b1e0b1e15332aa7af5a278738bd6" - integrity sha512-s4GyEQpZltaKc1BqQcGKWc8PbDfZzg0dlNDzlRxJ9f1kDLiU7/uZQFKOXcwUhYy3GgM68UPNwnNn0K9boQlMFA== + version "1.3.0-dev.4" + resolved "https://registry.npmjs.org/@keep-network/tbtc-v2.ts/-/tbtc-v2.ts-1.3.0-dev.4.tgz#7e0c62948b57f9506bf21d5268cd3c0675f4c712" + integrity sha512-M/lhApMkRoTZzYP+k02194KEkkN3T+eKQDClbL/+rO+86qBZOmO0y0CgsZyzv1HlUBjUa5X3dnd1zp6qf2qIqg== dependencies: - "@keep-network/ecdsa" "2.1.0-dev.11" - "@keep-network/tbtc-v2" "1.5.0-dev.2" + "@keep-network/ecdsa" "2.1.0-dev.14" + "@keep-network/tbtc-v2" "1.5.0-dev.5" bcoin "git+https://github.com/keep-network/bcoin.git#5accd32c63e6025a0d35d67739c4a6e84095a1f8" bcrypto "git+https://github.com/bcoin-org/bcrypto.git#semver:~5.5.0" bufio "^1.0.6" @@ -3175,7 +3206,20 @@ p-timeout "^4.1.0" wif "2.0.6" -"@keep-network/tbtc-v2@1.5.0-dev.2", "@keep-network/tbtc-v2@development": +"@keep-network/tbtc-v2@1.5.0-dev.5": + version "1.5.0-dev.5" + resolved "https://registry.npmjs.org/@keep-network/tbtc-v2/-/tbtc-v2-1.5.0-dev.5.tgz#a8391801ca9ea44a5e961002e721158bcdad855c" + integrity sha512-l+3vbWE8HdOxH1TwfU/qr1q7F/TmJByDq6INrQx1lca1zvjjOo+4d/qgv8CjcH9UZFg8D2BE56zaqhUl8hARLw== + dependencies: + "@keep-network/bitcoin-spv-sol" "3.4.0-solc-0.8" + "@keep-network/ecdsa" "2.1.0-dev.14" + "@keep-network/random-beacon" "2.1.0-dev.15" + "@keep-network/tbtc" "1.1.2-dev.1" + "@openzeppelin/contracts" "^4.8.1" + "@openzeppelin/contracts-upgradeable" "^4.8.1" + "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" + +"@keep-network/tbtc-v2@development": version "1.5.0-dev.2" resolved "https://registry.yarnpkg.com/@keep-network/tbtc-v2/-/tbtc-v2-1.5.0-dev.2.tgz#2e5b6adb5b265e998e2d296883c68d6587a7fedb" integrity sha512-L3lPNzVb1N6uEMABixx/5hR3t6dxGFMEuuCVWqaQ/jTocODru+0pwd5obYoD9aNIqtzuboezoBBorZsOoheiPw== @@ -3299,6 +3343,11 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.5.2.tgz#90d9e47bacfd8693bfad0ac8a394645575528d05" integrity sha512-xgWZYaPlrEOQo3cBj97Ufiuv79SPd8Brh4GcFYhPgb6WvAq4ppz8dWKL6h+jLAK01rUqMRp/TS9AdXgAeNvCLA== +"@openzeppelin/contracts@4.7.3": + version "4.7.3" + resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz#939534757a81f8d69cc854c7692805684ff3111e" + integrity sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw== + "@openzeppelin/contracts@^2.4.0": version "2.5.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-2.5.1.tgz#c76e3fc57aa224da3718ec351812a4251289db31" @@ -5766,8 +5815,8 @@ bech32@^2.0.0: bsert "~0.0.10" "bfile@git+https://github.com/bcoin-org/bfile.git#semver:~0.2.1": - version "0.2.2" - resolved "git+https://github.com/bcoin-org/bfile.git#c3075133a02830dc384f8353d8275d4499b8bff9" + version "0.2.3" + resolved "git+https://github.com/bcoin-org/bfile.git#c13235d04974f0fa5a487fdbaf74611523e2f4e6" "bfilter@git+https://github.com/bcoin-org/bfilter.git#semver:~2.3.0": version "2.3.0" @@ -6218,16 +6267,28 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -"bsert@git+https://github.com/chjj/bsert.git#semver:~0.0.10", bsert@~0.0.10: +"bsert@git+https://github.com/chjj/bsert.git#semver:~0.0.10": version "0.0.10" resolved "git+https://github.com/chjj/bsert.git#bd09d49eab8644bca08ae8259a3d8756e7d453fc" -"bsock@git+https://github.com/bcoin-org/bsock.git#semver:~0.1.9", bsock@~0.1.8, bsock@~0.1.9: +bsert@~0.0.10: + version "0.0.12" + resolved "https://registry.npmjs.org/bsert/-/bsert-0.0.12.tgz#157c6a6beb1548af3b14d484fcd2a78eb440599d" + integrity sha512-lUB0EMu4KhIf+VQ6RZJ7J3dFdohYSeta+gNgDi00Hi/t3k/W6xZlwm9PSSG0q7hJ2zW9Rsn5yaMPymETxroTRw== + +"bsock@git+https://github.com/bcoin-org/bsock.git#semver:~0.1.9": version "0.1.9" resolved "git+https://github.com/bcoin-org/bsock.git#7cf76b3021ae7929c023d1170f789811e91ae528" dependencies: bsert "~0.0.10" +bsock@~0.1.8, bsock@~0.1.9: + version "0.1.9" + resolved "https://registry.npmjs.org/bsock/-/bsock-0.1.9.tgz#6aa14b8e4bda730e0f60ec73eb52a8a888ac22c8" + integrity sha512-/l9Kg/c5o+n/0AqreMxh2jpzDMl1ikl4gUxT7RFNe3A3YRIyZkiREhwcjmqxiymJSRI/Qhew357xGn1SLw/xEw== + dependencies: + bsert "~0.0.10" + "bsocks@git+https://github.com/bcoin-org/bsocks.git#semver:~0.2.6": version "0.2.6" resolved "git+https://github.com/bcoin-org/bsocks.git#6a8eb764dc4408e7f47da4f84e1afb1b393117e8" @@ -6330,18 +6391,13 @@ bufferutil@^4.0.1: bufio@^1.0.6: version "1.2.0" - resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.2.0.tgz#b9ad1c06b0d9010363c387c39d2810a7086d143f" + resolved "https://registry.npmjs.org/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@git+https://github.com/bcoin-org/bufio.git#semver:~1.0.6", bufio@~1.0.7: 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" @@ -12317,7 +12373,7 @@ loader-utils@^2.0.0: loady@~0.0.1, loady@~0.0.5: version "0.0.5" - resolved "https://registry.yarnpkg.com/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881" + resolved "https://registry.npmjs.org/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881" integrity sha512-uxKD2HIj042/HBx77NBcmEPsD+hxCgAtjEWlYNScuUjIsh/62Uyu39GOR68TBR68v+jqDL9zfftCWoUo4y03sQ== locate-path@^2.0.0: @@ -13637,7 +13693,7 @@ p-timeout@^1.1.1: p-timeout@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw== p-try@^1.0.0: From e0fddeec539113e5552afd7d3648de9b1f2133fe Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 10:22:19 +0200 Subject: [PATCH 03/17] Update `TBTC` interface in `threshold-ts` lib Add `getBitcoinTransaction` method. To display the redemption details we need to fetch the Bitcoin transaction to find whether the redemption request was already handled successfully- meaning there was a Bitcoin transfer to a given redeemer output script. --- src/threshold-ts/tbtc/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 02e11c5ec..08ae89809 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -22,6 +22,7 @@ import { Client, computeHash160, decodeBitcoinAddress, + Transaction as BitcoinTransaction, TransactionHash, UnspentTransactionOutput, } from "@keep-network/tbtc-v2.ts/dist/src/bitcoin" @@ -198,6 +199,8 @@ export interface ITBTC { ): string findAllRevealedDeposits(depositor: string): Promise + + getBitcoinTransaction(transactionHash: string): Promise } export class TBTC implements ITBTC { @@ -629,4 +632,12 @@ export class TBTC implements ITBTC { depositOutputIndex ) } + + getBitcoinTransaction = async ( + transacionHash: string + ): Promise => { + return this._bitcoinClient.getTransaction( + TransactionHash.from(transacionHash) + ) + } } From 3306d941e564b52ae5d28f7e40a0f2fd7548c7b2 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 10:32:52 +0200 Subject: [PATCH 04/17] Update the `TBTC` interface in `threshold-ts` lib Add `buildRedemptionKey` method- builds the redemption key required to refer a redemption request. Redemption key built as `keccak256(keccak256(redeemerOutputScript) | walletPubKeyHash)`. --- src/threshold-ts/tbtc/index.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 08ae89809..53d663f56 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -34,7 +34,7 @@ import { BitcoinConfig, BitcoinNetwork, EthereumConfig } from "../types" 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" @@ -200,6 +200,19 @@ export interface ITBTC { findAllRevealedDeposits(depositor: 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 + getBitcoinTransaction(transactionHash: string): Promise } @@ -633,6 +646,19 @@ export class TBTC implements ITBTC { ) } + buildRedemptionKey = ( + walletPublicKeyHash: string, + redeemerOutputScript: string + ) => { + return utils.solidityKeccak256( + ["bytes32", "bytes20"], + [ + utils.solidityKeccak256(["bytes"], [redeemerOutputScript]), + walletPublicKeyHash, + ] + ) + } + getBitcoinTransaction = async ( transacionHash: string ): Promise => { From 75c268b624c1706a24e502e6d814a256a13c1a7c Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 11:04:52 +0200 Subject: [PATCH 05/17] Add methods that fetch redemption events and data To fetch the redemption details data we need query events to find the redemption request and verify whether the redemption was handled successfully or timed out. 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. --- src/threshold-ts/tbtc/index.ts | 259 ++++++++++++++++++++++++++++++++- 1 file changed, 256 insertions(+), 3 deletions(-) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 53d663f56..cdb239f0f 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -17,6 +17,7 @@ import { ZERO, isPublicKeyHashTypeAddress, isSameETHAddress, + AddressZero, } from "../utils" import { Client, @@ -29,6 +30,7 @@ import { import { ElectrumClient, EthereumBridge, + Hex, } from "@keep-network/tbtc-v2.ts/dist/src" import { BitcoinConfig, BitcoinNetwork, EthereumConfig } from "../types" import TBTCVault from "@keep-network/tbtc-v2/artifacts/TBTCVault.json" @@ -36,8 +38,8 @@ import Bridge from "@keep-network/tbtc-v2/artifacts/Bridge.json" import TBTCToken from "@keep-network/tbtc-v2/artifacts/TBTC.json" 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" export enum BridgeActivityStatus { PENDING = "PENDING", @@ -52,7 +54,7 @@ export interface BridgeActivity { depositKey: string } -interface RevealedDepositEvent { +export interface RevealedDepositEvent { amount: string walletPublicKeyHash: string fundingTxHash: string @@ -62,6 +64,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 interface ITBTC { @@ -214,6 +266,20 @@ export interface ITBTC { ): string getBitcoinTransaction(transactionHash: string): Promise + + getRedemptionRequestedEvents( + filter: RedemptionRequestedEventFilter + ): Promise + + getRedemptionRequest(redemptionKey: string): Promise + + getRedemptionTimedOutEvents( + filter: RedemptionTimedOutEventFilter + ): Promise + + getRedemptionsCompletedEvents( + filter: RedemptionsCompletedEventFilter + ): Promise } export class TBTC implements ITBTC { @@ -386,7 +452,7 @@ export class TBTC implements ITBTC { }> => { const calls: ContractCall[] = [ { - interface: new Interface(Bridge.abi), + interface: this._bridgeContract.interface, address: Bridge.address, method: "depositParameters", args: [], @@ -666,4 +732,191 @@ export class TBTC implements ITBTC { 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?.redeemer.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 + + 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 + + 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, + })) + } } From 499c1e61b324a01f5d0ba211634e7db2cbdb3e92 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 11:17:11 +0200 Subject: [PATCH 06/17] Create `useFetchRedemptionDetails` hook 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, - 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. --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 223 ++++++++++++++++++++ src/pages/tBTC/Bridge/UnmintDetails.tsx | 15 ++ 2 files changed, 238 insertions(+) create mode 100644 src/hooks/tbtc/useFetchRedemptionDetails.ts diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts new file mode 100644 index 000000000..864f02c66 --- /dev/null +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -0,0 +1,223 @@ +import { useEffect, useState } from "react" +import { useThreshold } from "../../contexts/ThresholdContext" +import { useGetBlock } from "../../web3/hooks" + +interface RedemptionDetails { + amount: string + redemptionRequestedTxHash: string + redemptionCompletedTxHash?: { + chain: string + bitcoin: string + } + requestedAt: number + completedAt: number + isTimedOut: boolean + redemptionTimedOutTxHash?: 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 `reedemer` 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 + ) + + console.log("redemptionRequest", redemptionRequest) + + 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 + ) + ) + + console.log( + "redemptionRequestedEventTimestamp", + redemptionRequestedEventTimestamp, + requestedAt + ) + + // 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 sucesfully 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, + completedAt: 0, + redemptionTimedOutTxHash: timedOutTxHash, + isTimedOut, + }) + return + } + + // If we are here it menas that the redemption request was handled + // successfully and we need to find all `RedemptionCompleted` events + // that happend after `redemptionRequest` block and filter by + // `walletPubKeyHash` param. + const redemptionCompletedEvents = + await threshold.tbtc.getRedemptionsCompletedEvents({ + walletPublicKeyHash, + fromBlock: redemptionRequest.blockNumber, + }) + + console.log("redemptionCompletedEvents", redemptionCompletedEvents) + + // 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) { + // TODO: compare correctly. + if ( + scriptPubKey.toString() !== redemptionRequest.redeemerOutputScript + ) + continue + + const { timestamp: redemptionCompletedTimestamp } = await getBlock( + redemptionCompletedBlockNumber + ) + setRedemptionData({ + amount: redemptionRequest.amount, + redemptionRequestedTxHash: redemptionRequest.txHash, + redemptionCompletedTxHash: { + chain: txHash, + bitcoin: redemptionBitcoinTxHash, + }, + requestedAt: requestedAt, + completedAt: redemptionCompletedTimestamp, + isTimedOut: false, + }) + + 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/UnmintDetails.tsx b/src/pages/tBTC/Bridge/UnmintDetails.tsx index 467e8dc34..b3a480128 100644 --- a/src/pages/tBTC/Bridge/UnmintDetails.tsx +++ b/src/pages/tBTC/Bridge/UnmintDetails.tsx @@ -49,6 +49,7 @@ import { ONE_SEC_IN_MILISECONDS } from "../../../utils/date" import { CopyAddressToClipboard } from "../../../components/CopyToClipboard" import { ProcessCompletedBrandGradientIcon } from "./components/BridgeProcessDetailsIcons" import { featureFlags } from "../../../constants" +import { useFetchRedemptionDetails } from "../../../hooks/tbtc/useFetchRedemptionDetails" export const UnmintDetails: PageComponent = () => { // TODO: Fetch redemption details by redemption key. @@ -76,6 +77,20 @@ export const UnmintDetails: PageComponent = () => { const btcAddress = "bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h" const fee = "20000000000000000" + const redemptionRequestedTxHash = + "0xef7219802103bdca0dc2d3dcc35d906cfff32708b5630a62aee9a72c49b94c3a" + const walletPublicKeyHash = "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e" + const redeemerOutputScript = + "0x17A914538E4CC700D6510C8CAE5E8B688D65276771E60887" + const redeemer = "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc" + + const { data } = useFetchRedemptionDetails( + redemptionRequestedTxHash, + walletPublicKeyHash, + redeemerOutputScript, + redeemer + ) + const transactions: { label: string txHash?: string From f5ee0e635777234b4ac13fe59f6cdfa62fccb302 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 11:40:38 +0200 Subject: [PATCH 07/17] Update `useFetchRedemptionDetails` hook Compare correctly the `scriptPubKey` data from Bitcoin transacion outputs with the redeemer output script from Ethereum event. The redeemer otput script from the Ethereum event is prepended by the script length encoded as a Bitcoin varint but the `scriptPubKey` is in a raw format so we need to prefix the output script bytes buffer with 0x and its own length. Here we also hardcoded data in the `UnmintDetails` component for a redemption that was already handled successfully- for testing purposes. --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 14 ++++++++++++-- src/pages/tBTC/Bridge/UnmintDetails.tsx | 5 +++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts index 864f02c66..d8d7c95c1 100644 --- a/src/hooks/tbtc/useFetchRedemptionDetails.ts +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -170,9 +170,9 @@ export const useFetchRedemptionDetails = ( ) for (const { scriptPubKey } of outputs) { - // TODO: compare correctly. if ( - scriptPubKey.toString() !== redemptionRequest.redeemerOutputScript + toPrefixedRawRedeemerOutputScript(scriptPubKey.toString()) !== + redemptionRequest.redeemerOutputScript ) continue @@ -221,3 +221,13 @@ export const useFetchRedemptionDetails = ( return { isFetching, data: redemptionData, error } } + +const toPrefixedRawRedeemerOutputScript = (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/pages/tBTC/Bridge/UnmintDetails.tsx b/src/pages/tBTC/Bridge/UnmintDetails.tsx index b3a480128..89173c141 100644 --- a/src/pages/tBTC/Bridge/UnmintDetails.tsx +++ b/src/pages/tBTC/Bridge/UnmintDetails.tsx @@ -78,10 +78,10 @@ export const UnmintDetails: PageComponent = () => { const fee = "20000000000000000" const redemptionRequestedTxHash = - "0xef7219802103bdca0dc2d3dcc35d906cfff32708b5630a62aee9a72c49b94c3a" + "0x0b5d66b89c5fe276ac5b0fd1874142f99329ea6f66485334a558e2bccd977618" const walletPublicKeyHash = "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e" const redeemerOutputScript = - "0x17A914538E4CC700D6510C8CAE5E8B688D65276771E60887" + "0x17A91486884E6BE1525DAB5AE0B451BD2C72CEE67DCF4187" const redeemer = "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc" const { data } = useFetchRedemptionDetails( @@ -90,6 +90,7 @@ export const UnmintDetails: PageComponent = () => { redeemerOutputScript, redeemer ) + console.log("data", data) const transactions: { label: string From 46c133e16704b1ac857a23e44f55a58bc45dc19c Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 15:30:00 +0200 Subject: [PATCH 08/17] Display redemption details data Display the redemption details data with real on-chain data. Based on that data we render different states of the unminting process. --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 16 +- src/pages/tBTC/Bridge/DepositDetails.tsx | 19 +- src/pages/tBTC/Bridge/UnmintDetails.tsx | 369 ++++++++++-------- .../BridgeProcessDetailsPageSkeleton.tsx | 20 + 4 files changed, 239 insertions(+), 185 deletions(-) create mode 100644 src/pages/tBTC/Bridge/components/BridgeProcessDetailsPageSkeleton.tsx diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts index d8d7c95c1..72c9eca65 100644 --- a/src/hooks/tbtc/useFetchRedemptionDetails.ts +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -10,9 +10,10 @@ interface RedemptionDetails { bitcoin: string } requestedAt: number - completedAt: number + completedAt?: number isTimedOut: boolean redemptionTimedOutTxHash?: string + btcAddress?: string } export const useFetchRedemptionDetails = ( @@ -95,12 +96,6 @@ export const useFetchRedemptionDetails = ( ) ) - console.log( - "redemptionRequestedEventTimestamp", - redemptionRequestedEventTimestamp, - requestedAt - ) - // Find the transaction hash where the timeout was reported by // scanning the `RedemptionTimedOut` event by the `walletPubKeyHash` // param. @@ -137,7 +132,6 @@ export const useFetchRedemptionDetails = ( redemptionRequestedTxHash: redemptionRequest.txHash, redemptionCompletedTxHash: undefined, requestedAt: requestedAt, - completedAt: 0, redemptionTimedOutTxHash: timedOutTxHash, isTimedOut, }) @@ -154,8 +148,6 @@ export const useFetchRedemptionDetails = ( fromBlock: redemptionRequest.blockNumber, }) - console.log("redemptionCompletedEvents", redemptionCompletedEvents) - // 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 @@ -186,9 +178,11 @@ export const useFetchRedemptionDetails = ( chain: txHash, bitcoin: redemptionBitcoinTxHash, }, - requestedAt: requestedAt, + requestedAt: redemptionRequestedEventTimestamp, completedAt: redemptionCompletedTimestamp, isTimedOut: false, + // TODO: convert the `scriptPubKey` to address. + btcAddress: "2Mzs2YNphdHmBoE7SE77cGB57JBXveNGtae", }) return 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 89173c141..fc7b812a4 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,11 +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. @@ -57,48 +75,51 @@ 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) - - return () => { - clearTimeout(id) - } - }, []) - - // TODO: check if the process is completed based on the redemptions details - // data. - // const isProcessCompleted = true - const unmintedAmount = "1200000000000000000" - const btcAddress = "bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h" - const fee = "20000000000000000" - - const redemptionRequestedTxHash = - "0x0b5d66b89c5fe276ac5b0fd1874142f99329ea6f66485334a558e2bccd977618" - const walletPublicKeyHash = "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e" - const redeemerOutputScript = - "0x17A91486884E6BE1525DAB5AE0B451BD2C72CEE67DCF4187" - const redeemer = "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc" + const { + redemptionRequestedTxHash, + walletPublicKeyHash, + redeemerOutputScript, + redeemer, + } = pendingRedemption - const { data } = useFetchRedemptionDetails( + const { data, isFetching, error } = useFetchRedemptionDetails( redemptionRequestedTxHash, walletPublicKeyHash, redeemerOutputScript, redeemer ) - console.log("data", data) + + const _isFetching = (isFetching || !data) && !error + const wasDataFetched = !isFetching && !!data && !error + + const btcTxHash = data?.redemptionCompletedTxHash?.bitcoin + useEffect(() => { + if (!!btcTxHash) setShouldDisplaySuccessStep(true) + }, [btcTxHash]) + + 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", + }, ] return ( @@ -109,104 +130,113 @@ export const UnmintDetails: PageComponent = () => { 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 && ( + + )} + )} @@ -266,6 +304,7 @@ const SuccessStep: FC<{ label="Unminted Amount" tokenAmount={unmintedAmount} tokenSymbol="tBTC" + tokenDecimals={8} precision={6} higherPrecision={8} /> @@ -291,6 +330,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 ( + <> + + + + + + + + + ) +} From f2003f61fb94d012711d2bf97260aecaa3aee6c4 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 15:33:43 +0200 Subject: [PATCH 09/17] Remove unnecessary console log --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts index 72c9eca65..0ec9921de 100644 --- a/src/hooks/tbtc/useFetchRedemptionDetails.ts +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -77,8 +77,6 @@ export const useFetchRedemptionDetails = ( ) === redemptionKey ) - console.log("redemptionRequest", redemptionRequest) - if (!redemptionRequest) { throw new Error("Redemption not found...") } From a7e13840c5c6f6df585c60e0c5c6a4c3e7911f4a Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 23:10:52 +0200 Subject: [PATCH 10/17] Fix typos in comments --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts index 0ec9921de..f335153a0 100644 --- a/src/hooks/tbtc/useFetchRedemptionDetails.ts +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -40,9 +40,9 @@ export const useFetchRedemptionDetails = ( ) // We need to find `RedemptionRequested` event by wallet public key hash - // and `reedemer` address to get all necessary data and make sure that + // 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 + // reduce the number of records - any user can request redemption for the // same wallet. const redemptionRequest = ( await threshold.tbtc.getRedemptionRequestedEvents({ @@ -52,7 +52,7 @@ export const useFetchRedemptionDetails = ( ).find( (event) => // It's not possible that the redemption request with the same - // redemption key can be created in the same transaction- it means + // 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 @@ -113,7 +113,7 @@ export const useFetchRedemptionDetails = ( // We need to make sure this is the same redemption request. Let's // consider this case: // - redemption X requested, - // - redemption X was handled sucesfully and the redemption X was + // - 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 @@ -136,9 +136,9 @@ export const useFetchRedemptionDetails = ( return } - // If we are here it menas that the redemption request was handled + // If we are here it means that the redemption request was handled // successfully and we need to find all `RedemptionCompleted` events - // that happend after `redemptionRequest` block and filter by + // that happened after `redemptionRequest` block and filter by // `walletPubKeyHash` param. const redemptionCompletedEvents = await threshold.tbtc.getRedemptionsCompletedEvents({ From 990ea355f10421ddc16d065b844edde61e0d0fad Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 23:12:31 +0200 Subject: [PATCH 11/17] Fix warning in console We should assign useColorModeValue("white", "brand.800") to the variable on top of the component. --- src/pages/tBTC/Bridge/UnmintDetails.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/tBTC/Bridge/UnmintDetails.tsx b/src/pages/tBTC/Bridge/UnmintDetails.tsx index fc7b812a4..0a67d2984 100644 --- a/src/pages/tBTC/Bridge/UnmintDetails.tsx +++ b/src/pages/tBTC/Bridge/UnmintDetails.tsx @@ -122,6 +122,8 @@ export const UnmintDetails: PageComponent = () => { }, ] + const timelineBadgeBgColor = useColorModeValue("white", "brand.800") + return ( { Date: Fri, 7 Jul 2023 23:14:15 +0200 Subject: [PATCH 12/17] Fix link to BTC address on unmint details page BTC address on success page redirects to the etherscan instead of blockstream. Here we add `chain="bitcoin"` prop to fix this bug. --- src/pages/tBTC/Bridge/UnmintDetails.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tBTC/Bridge/UnmintDetails.tsx b/src/pages/tBTC/Bridge/UnmintDetails.tsx index 0a67d2984..4ff9f59ce 100644 --- a/src/pages/tBTC/Bridge/UnmintDetails.tsx +++ b/src/pages/tBTC/Bridge/UnmintDetails.tsx @@ -320,6 +320,7 @@ const SuccessStep: FC<{ From 275749da3e207d009394b8cc879a4b1980620c0a Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 23:18:10 +0200 Subject: [PATCH 13/17] Fix `_parseRedemptionRequestedEvent` method We should assign `event.ergs?.treasuryFee` instead of `event.ergs?.redeemer` to the `treasuryFee`. --- 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 cdb239f0f..b00e19cf5 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -812,7 +812,7 @@ export class TBTC implements ITBTC { walletPublicKeyHash: event.args?.walletPubKeyHash.toString(), redeemerOutputScript: event.args?.redeemerOutputScript.toString(), redeemer: event.args?.redeemer.toString(), - treasuryFee: event.args?.redeemer.toString(), + treasuryFee: event.args?.treasuryFee.toString(), txMaxFee: event.args?.txMaxFee.toString(), blockNumber: event.blockNumber, txHash: event.transactionHash, From 1cac4b9ef4d986ec1e6e76eb3d404d519c19d9eb Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 23:23:10 +0200 Subject: [PATCH 14/17] Leave TODOs in TBTC class Mention in comments that the current code is a workaround and we should use `getContractPastEvents` to fetch events one we provide a fix in the `ethers.js` lib. The `ethers.js` lib encodes the `bytesX` param in the wrong way. --- src/threshold-ts/tbtc/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index b00e19cf5..a157523a2 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -869,6 +869,10 @@ export class TBTC implements ITBTC { ): 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), @@ -896,6 +900,10 @@ export class TBTC implements ITBTC { ): 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), From febbedcb7be098edffb79f47ea83012e77094394 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 23:34:29 +0200 Subject: [PATCH 15/17] Extract function to `threshold-ts` lib utils Move function that prefixes the output script with `0x` and its own length to `threshold-ts` lib utils. --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 13 ++----------- src/threshold-ts/utils/bitcoin.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts index f335153a0..1be37787c 100644 --- a/src/hooks/tbtc/useFetchRedemptionDetails.ts +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react" import { useThreshold } from "../../contexts/ThresholdContext" +import { prependScriptPubKeyByLength } from "../../threshold-ts/utils" import { useGetBlock } from "../../web3/hooks" interface RedemptionDetails { @@ -161,7 +162,7 @@ export const useFetchRedemptionDetails = ( for (const { scriptPubKey } of outputs) { if ( - toPrefixedRawRedeemerOutputScript(scriptPubKey.toString()) !== + prependScriptPubKeyByLength(scriptPubKey.toString()) !== redemptionRequest.redeemerOutputScript ) continue @@ -213,13 +214,3 @@ export const useFetchRedemptionDetails = ( return { isFetching, data: redemptionData, error } } - -const toPrefixedRawRedeemerOutputScript = (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/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")}` +} From 9776847fd360bfc3653fb2b99ff00d56920635c9 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 11 Jul 2023 14:40:41 +0200 Subject: [PATCH 16/17] Break too long comment to next line --- src/hooks/tbtc/useFetchRedemptionDetails.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/tbtc/useFetchRedemptionDetails.ts b/src/hooks/tbtc/useFetchRedemptionDetails.ts index 1be37787c..d0a1b61b4 100644 --- a/src/hooks/tbtc/useFetchRedemptionDetails.ts +++ b/src/hooks/tbtc/useFetchRedemptionDetails.ts @@ -43,8 +43,8 @@ export const useFetchRedemptionDetails = ( // 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. + // reduce the number of records - any user can request redemption for + // the same wallet. const redemptionRequest = ( await threshold.tbtc.getRedemptionRequestedEvents({ walletPublicKeyHash, From ed3e3c4e14aef58563d08a0f7102a7b17e69e839 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 11 Jul 2023 14:56:21 +0200 Subject: [PATCH 17/17] Add missing docs comments --- src/threshold-ts/tbtc/index.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 228fded40..0adc1c4ba 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -329,18 +329,47 @@ export interface ITBTC { 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