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") + } }