From 97d3c65776fb95d95c4e0976004f708865d14f29 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 20 Jun 2023 14:53:10 +0200 Subject: [PATCH] Add unit tests for `findWalletForRedemption` fn --- typescript/test/data/redemption.ts | 121 +++++++++++++++++++++++ typescript/test/redemption.test.ts | 138 +++++++++++++++++++++++++++ typescript/test/utils/mock-bridge.ts | 38 +++++++- 3 files changed, 294 insertions(+), 3 deletions(-) diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 8a361c746..0080de3a2 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -11,6 +11,7 @@ import { import { RedemptionRequest } from "../../src/redemption" import { Address } from "../../src/ethereum" import { Hex } from "../../src" +import { NewWalletRegisteredEvent, WalletState } from "../../src/wallet" /** * Private key (testnet) of the wallet. @@ -666,3 +667,123 @@ export const redemptionProof: RedemptionProofTestData = { "03989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9", }, } + +export const findWalletForRedemptionData: { + newWalletRegisteredEvents: NewWalletRegisteredEvent[] + wallets: { + [walletPublicKeyHash: string]: { + state: WalletState + mainUtxoHash: Hex + walletPublicKey: Hex + btcAddress: string + utxos: UnspentTransactionOutput[] + } + } +} = { + newWalletRegisteredEvents: [ + { + blockNumber: 8367602, + blockHash: Hex.from( + "0x908ea9c82b388a760e6dd070522e5421d88b8931fbac6702119f9e9a483dd022" + ), + transactionHash: Hex.from( + "0xc1e995d0ac451cc9ffc9d43f105eddbaf2eb45ea57a61074a84fc022ecf5bda9" + ), + ecdsaWalletID: Hex.from( + "0x5314e0e5a62b173f52ea424958e5bc04bd77e2159478934a89d4fa193c7b3b72" + ), + walletPublicKeyHash: Hex.from( + "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e" + ), + }, + { + blockNumber: 8502240, + blockHash: Hex.from( + "0x4baab7520cf79a05f22723688bcd1f2805778829aa4362250b8ee702f34f4daf" + ), + transactionHash: Hex.from( + "0xe88761c7203335e237366ec2ffca1e7cf2690eab343ad700e6a6e6dc236638b1" + ), + ecdsaWalletID: Hex.from( + "0x0c70f262eaff2cdaaddb5a5e4ecfdda6edad7f1789954ad287bfa7e594173c64" + ), + walletPublicKeyHash: Hex.from( + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed" + ), + }, + { + blockNumber: 8981644, + blockHash: Hex.from( + "0x6681b1bb168fb86755c2a796169cb0e06949caac9fc7145d527d94d5209a64ad" + ), + transactionHash: Hex.from( + "0xea3a8853c658145c95165d7847152aeedc3ff29406ec263abfc9b1436402b7b7" + ), + ecdsaWalletID: Hex.from( + "0x7a1437d67f49adfd44e03ddc85be0f6988715d7c39dfb0ca9780f1a88bcdca25" + ), + walletPublicKeyHash: Hex.from( + "0x328d992e5f5b71de51a1b40fcc4056b99a88a647" + ), + }, + ], + wallets: { + "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e": { + state: WalletState.Live, + mainUtxoHash: Hex.from( + "0x3ded9dcfce0ffe479640013ebeeb69b6a82306004f9525b1346ca3b553efc6aa" + ), + walletPublicKey: Hex.from( + "0x028ed84936be6a9f594a2dcc636d4bebf132713da3ce4dac5c61afbf8bbb47d6f7" + ), + btcAddress: "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja", + utxos: [ + { + transactionHash: Hex.from( + "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" + ), + outputIndex: 0, + value: BigNumber.from("791613461"), + }, + ], + }, + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed": { + state: WalletState.Live, + mainUtxoHash: Hex.from( + "0x3ea242dd8a7f7f7abd548ca6590de70a1e992cbd6e4ae18b7a91c9b899067626" + ), + walletPublicKey: Hex.from( + "0x025183c15164e1b2211eb359fce2ceeefc3abad3af6d760cc6355f9de99bf60229" + ), + btcAddress: "tb1qwecrg07qpnxz6rxk2dswdt2qq6t75rldweydm2", + utxos: [ + { + transactionHash: Hex.from( + "0xda0e364abb3ed952bcc694e48bbcff19131ba9513fe981b303fa900cff0f9fbc" + ), + outputIndex: 0, + value: BigNumber.from("164380000"), + }, + ], + }, + "0x328d992e5f5b71de51a1b40fcc4056b99a88a647": { + state: WalletState.Live, + mainUtxoHash: Hex.from( + "0xb3024ef698084cfdfba459338864a595d31081748b28aa5eb02312671a720531" + ), + walletPublicKey: Hex.from( + "0x02ab193a63b3523bfab77d3645d11da10722393687458c4213b350b7e08f50b7ee" + ), + btcAddress: "tb1qx2xejtjltdcau5dpks8ucszkhxdg3fj88404lh", + utxos: [ + { + transactionHash: Hex.from( + "0x81c4884a8c2fccbeb57745a5e59f895a9c1bb8fc42eecc82269100a1a46bbb85" + ), + outputIndex: 0, + value: BigNumber.from("3370000"), + }, + ], + }, + }, +} diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index a32a09ae3..45d2d186c 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -20,9 +20,11 @@ import { p2pkhWalletAddress, p2wpkhWalletAddress, RedemptionTestData, + findWalletForRedemptionData, } from "./data/redemption" import { assembleRedemptionTransaction, + findWalletForRedemption, getRedemptionRequest, RedemptionRequest, requestRedemption, @@ -34,6 +36,8 @@ import * as chai from "chai" import chaiAsPromised from "chai-as-promised" import { expect } from "chai" import { BigNumberish, BigNumber } from "ethers" +import { BitcoinNetwork } from "../src/bitcoin-network" +import { Wallet } from "../src/wallet" chai.use(chaiAsPromised) @@ -1432,6 +1436,140 @@ describe("Redemption", () => { }) }) }) + + describe("findWalletForRedemption", () => { + let bridge: MockBridge + let bitcoinClient: MockBitcoinClient + + context( + "when there are no wallets in the network that can hanlde redemption", + () => { + const amount: BigNumber = BigNumber.from("1000000") // 0.01 BTC + beforeEach(() => { + bitcoinClient = new MockBitcoinClient() + bridge = new MockBridge() + bridge.newWalletRegisteredEvents = [] + }) + + it("should throw an error", async () => { + await expect( + findWalletForRedemption( + amount, + bridge, + bitcoinClient, + BitcoinNetwork.Testnet + ) + ).to.be.rejectedWith( + "Could not find a wallet with enough funds. Maximum redemption amount is 0 Satoshi." + ) + }) + } + ) + + context("when there are registered wallets in the network", () => { + let result: Awaited | never> + + beforeEach(async () => { + bitcoinClient = new MockBitcoinClient() + bridge = new MockBridge() + bridge.newWalletRegisteredEvents = + findWalletForRedemptionData.newWalletRegisteredEvents + const walletsUnspentTransacionOutputs = new Map< + string, + UnspentTransactionOutput[] + >() + for (const [ + key, + { state, mainUtxoHash, walletPublicKey, btcAddress, utxos }, + ] of Object.entries(findWalletForRedemptionData.wallets)) { + bridge.setWallet(key, { + state, + mainUtxoHash, + walletPublicKey, + } as Wallet) + walletsUnspentTransacionOutputs.set(btcAddress, utxos) + } + + bitcoinClient.unspentTransactionOutputs = + walletsUnspentTransacionOutputs + }) + + context( + "when there is a wallet that can handle the redemption request", + () => { + const amount: BigNumber = BigNumber.from("1000000") // 0.01 BTC + beforeEach(async () => { + result = await findWalletForRedemption( + amount, + bridge, + bitcoinClient, + BitcoinNetwork.Testnet + ) + }) + + it("should get all registered wallets", () => { + const bridgeQueryEventsLog = bridge.newWalletRegisteredEventsLog + + expect(bridgeQueryEventsLog.length).to.equal(1) + expect(bridgeQueryEventsLog[0]).to.deep.equal({ + options: undefined, + filterArgs: [], + }) + }) + + it("should get wallet data details", () => { + const bridgeWalletDetailsLogs = bridge.walletsLog + + expect(bridgeWalletDetailsLogs.length).to.eql(1) + expect(bridgeWalletDetailsLogs[0].walletPublicKeyHash).to.eql( + findWalletForRedemptionData.newWalletRegisteredEvents[0].walletPublicKeyHash.toPrefixedString() + ) + }) + + it("should return the wallet data that can handle redemption request", () => { + const expectedWalletPublicKeyHash = + findWalletForRedemptionData.newWalletRegisteredEvents[0] + .walletPublicKeyHash + const expectedWalletData = + findWalletForRedemptionData.wallets[ + expectedWalletPublicKeyHash.toPrefixedString() + ] + expect(result).to.deep.eq({ + walletPublicKey: expectedWalletData.walletPublicKey.toString(), + mainUTXO: expectedWalletData.utxos[0], + }) + }) + } + ) + + context( + "when the redemption request amount is too large and no wallet can handle the request", + () => { + const amount = BigNumber.from("10000000000") // 1 000 BTC + const expectedMaxAmount = Object.values( + findWalletForRedemptionData.wallets + ) + .map((wallet) => wallet.utxos) + .flat() + .map((utxo) => utxo.value) + .sort((a, b) => (b.gt(a) ? 0 : -1))[0] + + it("should throw an error", async () => { + await expect( + findWalletForRedemption( + amount, + bridge, + bitcoinClient, + BitcoinNetwork.Testnet + ) + ).to.be.rejectedWith( + `Could not find a wallet with enough funds. Maximum redemption amount is ${expectedMaxAmount.toString()} Satoshi.` + ) + }) + } + ) + }) + }) }) async function runRedemptionScenario( diff --git a/typescript/test/utils/mock-bridge.ts b/typescript/test/utils/mock-bridge.ts index cb4e4064a..f9f9a7b36 100644 --- a/typescript/test/utils/mock-bridge.ts +++ b/typescript/test/utils/mock-bridge.ts @@ -43,6 +43,15 @@ interface RedemptionProofLogEntry { walletPublicKey: string } +interface NewWalletRegisteredEventsLog { + options?: GetEvents.Options + filterArgs: unknown[] +} + +interface WalletLog { + walletPublicKeyHash: string +} + /** * Mock Bridge used for test purposes. */ @@ -56,6 +65,10 @@ export class MockBridge implements Bridge { private _redemptionProofLog: RedemptionProofLogEntry[] = [] private _deposits = new Map() private _activeWalletPublicKey: string | undefined + private _newWalletRegisteredEvents: NewWalletRegisteredEvent[] = [] + private _newWalletRegisteredEventsLog: NewWalletRegisteredEventsLog[] = [] + private _wallets = new Map() + private _walletsLog: WalletLog[] = [] setPendingRedemptions(value: Map) { this._pendingRedemptions = value @@ -65,6 +78,14 @@ export class MockBridge implements Bridge { this._timedOutRedemptions = value } + setWallet(key: string, value: Wallet) { + this._wallets.set(key, value) + } + + set newWalletRegisteredEvents(value: NewWalletRegisteredEvent[]) { + this._newWalletRegisteredEvents = value + } + get depositSweepProofLog(): DepositSweepProofLogEntry[] { return this._depositSweepProofLog } @@ -81,6 +102,14 @@ export class MockBridge implements Bridge { return this._redemptionProofLog } + get newWalletRegisteredEventsLog(): NewWalletRegisteredEventsLog[] { + return this._newWalletRegisteredEventsLog + } + + get walletsLog(): WalletLog[] { + return this._walletsLog + } + setDeposits(value: Map) { this._deposits = value } @@ -308,15 +337,18 @@ export class MockBridge implements Bridge { options?: GetEvents.Options, ...filterArgs: Array ): Promise { - throw new Error("not implemented") + this._newWalletRegisteredEventsLog.push({ options, filterArgs }) + return this._newWalletRegisteredEvents } walletRegistry(): Promise { throw new Error("not implemented") } - wallets(walletPublicKeyHash: string): Promise { - throw new Error("not implemented") + async wallets(walletPublicKeyHash: string): Promise { + this._walletsLog.push({ walletPublicKeyHash }) + const wallet = this._wallets.get(walletPublicKeyHash) + return wallet! } buildUTXOHash(utxo: UnspentTransactionOutput): Hex {