From 34a7b099fb9b5d48fce402652b8e1daae1ff0b2c Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 1 Aug 2023 18:17:05 +0200 Subject: [PATCH 1/2] Expose past redemption requested events getter Here we expose the `Bridge.getRedemptionRequestedEvents` method that allows fetching past redemption requested events from the `Bridge` contract. --- typescript/src/bitcoin.ts | 40 ++++++++++++++++++++++++++++ typescript/src/chain.ts | 8 +++++- typescript/src/ethereum.ts | 39 ++++++++++++++++++++++++++- typescript/src/redemption.ts | 16 ++++++++++- typescript/test/bitcoin.test.ts | 38 ++++++++++++++++++++++++++ typescript/test/utils/mock-bridge.ts | 9 ++++++- 6 files changed, 146 insertions(+), 4 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 3d7c32585..a46a4792b 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -639,3 +639,43 @@ export function createAddressFromOutputScript( .getAddress() ?.toString(toBcoinNetwork(network)) } + +/** + * Reads the leading compact size uint from the provided variable length data. + * + * WARNING: CURRENTLY, THIS FUNCTION SUPPORTS ONLY 1-BYTE COMPACT SIZE UINTS + * AND WILL THROW ON COMPACT SIZE UINTS OF DIFFERENT BYTE LENGTH. + * + * @param varLenData Variable length data preceded by a compact size uint. + * @returns An object holding the value of the compact size uint along with the + * compact size uint byte length. + */ +export function readCompactSizeUint(varLenData: Hex): { + value: number + byteLength: number +} { + // The varLenData is prefixed with the compact size uint. According to the docs + // (https://developer.bitcoin.org/reference/transactions.html#compactsize-unsigned-integers) + // a compact size uint can be 1, 3, 5 or 9 bytes. To determine the exact length, + // we need to look at the discriminant byte which is always the first byte of + // the compact size uint. + const discriminant = varLenData.toString().slice(0, 2) + + switch (discriminant) { + case "ff": + case "fe": + case "fd": { + throw new Error( + "support for 3, 5 and 9 bytes compact size uints is not implemented yet" + ) + } + default: { + // The discriminant tells the compact size uint is 1 byte. That means + // the discriminant represent the value itself. + return { + value: parseInt(discriminant, 16), + byteLength: 1, + } + } + } +} diff --git a/typescript/src/chain.ts b/typescript/src/chain.ts index 5b6f619e6..3b4686dc5 100644 --- a/typescript/src/chain.ts +++ b/typescript/src/chain.ts @@ -17,7 +17,7 @@ import { OptimisticMintingRequestedEvent, } from "./optimistic-minting" import { Hex } from "./hex" -import { RedemptionRequest } from "./redemption" +import { RedemptionRequest, RedemptionRequestedEvent } from "./redemption" import { DkgResultApprovedEvent, DkgResultChallengedEvent, @@ -260,6 +260,12 @@ export interface Bridge { * @returns The hash of the UTXO. */ buildUtxoHash(utxo: UnspentTransactionOutput): Hex + + /** + * Get emitted RedemptionRequested events. + * @see GetEventsFunction + */ + getRedemptionRequestedEvents: GetEvents.Function } /** diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index 60490f906..f86c80798 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -27,12 +27,13 @@ import { DepositRevealedEvent, } from "./deposit" import { getEvents, sendWithRetry } from "./ethereum-helpers" -import { RedemptionRequest } from "./redemption" +import { RedemptionRequest, RedemptionRequestedEvent } from "./redemption" import { compressPublicKey, computeHash160, DecomposedRawTransaction, Proof, + readCompactSizeUint, TransactionHash, UnspentTransactionOutput, } from "./bitcoin" @@ -762,6 +763,42 @@ export class Bridge ) ) } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {ChainBridge#getDepositRevealedEvents} + */ + async getRedemptionRequestedEvents( + options?: GetEvents.Options, + ...filterArgs: Array + ): Promise { + const events: EthersEvent[] = await this.getEvents( + "RedemptionRequested", + options, + ...filterArgs + ) + + return events.map((event) => { + const prefixedRedeemerOutputScript = Hex.from( + event.args!.redeemerOutputScript + ) + const redeemerOutputScript = prefixedRedeemerOutputScript + .toString() + .slice(readCompactSizeUint(prefixedRedeemerOutputScript).byteLength * 2) + + return { + blockNumber: BigNumber.from(event.blockNumber).toNumber(), + blockHash: Hex.from(event.blockHash), + transactionHash: Hex.from(event.transactionHash), + walletPublicKeyHash: Hex.from(event.args!.walletPubKeyHash).toString(), + redeemer: new Address(event.args!.redeemer), + redeemerOutputScript: redeemerOutputScript, + requestedAmount: BigNumber.from(event.args!.requestedAmount), + treasuryFee: BigNumber.from(event.args!.treasuryFee), + txMaxFee: BigNumber.from(event.args!.txMaxFee), + } + }) + } } /** diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index f3b2474a0..ab713199a 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -8,7 +8,7 @@ import { Client as BitcoinClient, TransactionHash, } from "./bitcoin" -import { Bridge, Identifier, TBTCToken } from "./chain" +import { Bridge, Event, Identifier, TBTCToken } from "./chain" import { assembleTransactionProof } from "./proof" import { determineWalletMainUtxo, WalletState } from "./wallet" import { BitcoinNetwork } from "./bitcoin-network" @@ -56,6 +56,20 @@ export interface RedemptionRequest { requestedAt: number } +/** + * Represents an event emitted on redemption request. + */ +export type RedemptionRequestedEvent = Omit< + RedemptionRequest, + "requestedAt" +> & { + /** + * Public key hash of the wallet that is meant to handle the redemption. Must + * be an unprefixed hex string (without 0x prefix). + */ + walletPublicKeyHash: string +} & Event + /** * Requests a redemption of tBTC into BTC. * @param walletPublicKey - The Bitcoin public key of the wallet. Must be in the diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index 27ce2861f..748e39004 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -13,6 +13,7 @@ import { targetToDifficulty, createOutputScriptFromAddress, createAddressFromOutputScript, + readCompactSizeUint, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" @@ -506,4 +507,41 @@ describe("Bitcoin", () => { }) }) }) + + describe("readCompactSizeUint", () => { + context("when the compact size uint is 1-byte", () => { + it("should return the the uint value and byte length", () => { + expect(readCompactSizeUint(Hex.from("bb"))).to.be.eql({ + value: 187, + byteLength: 1, + }) + }) + }) + + context("when the compact size uint is 3-byte", () => { + it("should throw", () => { + expect(() => readCompactSizeUint(Hex.from("fd0302"))).to.throw( + "support for 3, 5 and 9 bytes compact size uints is not implemented yet" + ) + }) + }) + + context("when the compact size uint is 5-byte", () => { + it("should throw", () => { + expect(() => readCompactSizeUint(Hex.from("fe703a0f00"))).to.throw( + "support for 3, 5 and 9 bytes compact size uints is not implemented yet" + ) + }) + }) + + context("when the compact size uint is 9-byte", () => { + it("should throw", () => { + expect(() => { + return readCompactSizeUint(Hex.from("ff57284e56dab40000")) + }).to.throw( + "support for 3, 5 and 9 bytes compact size uints is not implemented yet" + ) + }) + }) + }) }) diff --git a/typescript/test/utils/mock-bridge.ts b/typescript/test/utils/mock-bridge.ts index 6a429c655..c18c0909f 100644 --- a/typescript/test/utils/mock-bridge.ts +++ b/typescript/test/utils/mock-bridge.ts @@ -5,7 +5,7 @@ import { UnspentTransactionOutput, } from "../../src/bitcoin" import { BigNumberish, BigNumber, utils, constants } from "ethers" -import { RedemptionRequest } from "../redemption" +import { RedemptionRequest, RedemptionRequestedEvent } from "../redemption" import { Deposit, DepositRevealedEvent, @@ -365,4 +365,11 @@ export class MockBridge implements Bridge { ) ) } + + getRedemptionRequestedEvents( + options?: GetEvents.Options, + ...filterArgs: Array + ): Promise { + throw new Error("not implemented") + } } From cce9d9c6df0dea1ec98b4c408ae68decf0ccbd03 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 2 Aug 2023 12:22:05 +0200 Subject: [PATCH 2/2] Add a note about ethers.js problem --- typescript/src/ethereum.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index f86c80798..6630d8b71 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -772,6 +772,12 @@ export class Bridge options?: GetEvents.Options, ...filterArgs: Array ): Promise { + // FIXME: Filtering by indexed walletPubKeyHash field may not work + // until https://github.com/ethers-io/ethers.js/pull/4244 is + // included in the currently used version of ethers.js. + // Ultimately, we should upgrade ethers.js to include that fix. + // Short-term, we can workaround the problem as presented in: + // https://github.com/threshold-network/token-dashboard/blob/main/src/threshold-ts/tbtc/index.ts#L1041C1-L1093C1 const events: EthersEvent[] = await this.getEvents( "RedemptionRequested", options,