From 751e81fa21af596462e84c8d0d3bab6d0f951ae6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 7 Jun 2023 14:42:20 +0200 Subject: [PATCH 01/23] Add the new fn that finds a wallet for redemption Add `findWalletForRedemption` function that returns the wallet details needed to request a redemption based on the redemption amount. This function looks for an oldest acive wallet(wallet must be in `Live` state) that has enough BTC to handle a redemption request. --- typescript/src/chain.ts | 16 +++++++ typescript/src/ethereum.ts | 68 ++++++++++++++++++++++++--- typescript/src/index.ts | 2 + typescript/src/redemption.ts | 69 ++++++++++++++++++++++++++++ typescript/test/utils/mock-bridge.ts | 19 +++++++- 5 files changed, 166 insertions(+), 8 deletions(-) diff --git a/typescript/src/chain.ts b/typescript/src/chain.ts index dae8606d2..cb7f2a873 100644 --- a/typescript/src/chain.ts +++ b/typescript/src/chain.ts @@ -23,6 +23,7 @@ import { DkgResultChallengedEvent, DkgResultSubmittedEvent, NewWalletRegisteredEvent, + Wallet, } from "./wallet" import type { ExecutionLoggerFn } from "./backoff" @@ -244,6 +245,21 @@ export interface Bridge { * Returns the attached WalletRegistry instance. */ walletRegistry(): Promise + + /** + * Gets details about a registered wallet. + * @param walletPublicKeyHash The 20-byte wallet public key hash (computed + * using Bitcoin HASH160 over the compressed ECDSA public key). + * @returns Promise with the wallet details. + */ + wallets(walletPublicKeyHash: string): Promise + + /** + * Builds the UTXO hash based on the UTXO components. + * @param utxo UTXO components. + * @returns The hash of the UTXO. + */ + buildUTXOHash(utxo: UnspentTransactionOutput): Hex } /** diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index 69cf0c86e..d64c98a4c 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -47,6 +47,7 @@ import type { Bridge as ContractBridge, Deposit as ContractDeposit, Redemption as ContractRedemption, + Wallets, } from "../typechain/Bridge" import type { WalletRegistry as ContractWalletRegistry } from "../typechain/WalletRegistry" import type { TBTCVault as ContractTBTCVault } from "../typechain/TBTCVault" @@ -57,6 +58,8 @@ import { DkgResultChallengedEvent, DkgResultSubmittedEvent, NewWalletRegisteredEvent, + Wallet, + WalletState, } from "./wallet" type ContractDepositRequest = ContractDeposit.DepositRequestStructOutput @@ -639,18 +642,18 @@ export class Bridge return undefined } - const { ecdsaWalletID } = await backoffRetrier<{ ecdsaWalletID: string }>( - this._totalRetryAttempts - )(async () => { - return await this._instance.wallets(activeWalletPublicKeyHash) - }) + const { ecdsaWalletID } = await this.wallets(activeWalletPublicKeyHash) + return (await this.toCompressedWalletPublicKey(ecdsaWalletID)).toString() + } + + private async toCompressedWalletPublicKey(ecdsaWalletID: Hex): Promise { const walletRegistry = await this.walletRegistry() const uncompressedPublicKey = await walletRegistry.getWalletPublicKey( - Hex.from(ecdsaWalletID) + ecdsaWalletID ) - return compressPublicKey(uncompressedPublicKey) + return Hex.from(compressPublicKey(uncompressedPublicKey)) } // eslint-disable-next-line valid-jsdoc @@ -694,6 +697,57 @@ export class Bridge signerOrProvider: this._instance.signer || this._instance.provider, }) } + + async wallets(walletPublicKeyHash: string): Promise { + const wallet = await backoffRetrier( + this._totalRetryAttempts + )(async () => { + return await this._instance.wallets(walletPublicKeyHash) + }) + + return this.parseWalletDetails(wallet) + } + + private async parseWalletDetails( + wallet: Wallets.WalletStructOutput + ): Promise { + const ecdsaWalletID = Hex.from(wallet.ecdsaWalletID) + + return { + ecdsaWalletID, + walletPublicKey: await this.toCompressedWalletPublicKey(ecdsaWalletID), + mainUtxoHash: Hex.from(wallet.mainUtxoHash), + pendingRedemptionsValue: wallet.pendingRedemptionsValue, + createdAt: wallet.createdAt, + movingFundsRequestedAt: wallet.movingFundsRequestedAt, + closingStartedAt: wallet.closingStartedAt, + pendingMovedFundsSweepRequestsCount: + wallet.pendingMovedFundsSweepRequestsCount, + state: WalletState.parse(wallet.state), + movingFundsTargetWalletsCommitmentHash: Hex.from( + wallet.movingFundsTargetWalletsCommitmentHash + ), + } + } + + /** + * Builds the UTXO hash based on the UTXO components. UTXO hash is computed as + * `keccak256(txHash | txOutputIndex | txOutputValue)`. + * @param utxo UTXO components. + * @returns The hash of the UTXO. + */ + buildUTXOHash(utxo: UnspentTransactionOutput): Hex { + return Hex.from( + utils.solidityKeccak256( + ["bytes32", "uint32", "uint64"], + [ + utxo.transactionHash.reverse().toPrefixedString(), + utxo.outputIndex, + utxo.value, + ] + ) + ) + } } /** diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 8f5384bde..7bc6e4922 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -13,6 +13,7 @@ import { requestRedemption, submitRedemptionProof, getRedemptionRequest, + findWalletForRedemption, } from "./redemption" import { @@ -31,6 +32,7 @@ export const TBTC = { getRevealedDeposit, requestRedemption, getRedemptionRequest, + findWalletForRedemption, } export const SpvMaintainer = { diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index ec96659d5..6256b3057 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -7,9 +7,12 @@ import { UnspentTransactionOutput, Client as BitcoinClient, TransactionHash, + encodeToBitcoinAddress, } from "./bitcoin" import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" +import { WalletState } from "./wallet" +import { BitcoinNetwork } from "./bitcoin-network" /** * Represents a redemption request. @@ -406,3 +409,69 @@ export async function getRedemptionRequest( return redemptionRequests[0] } + +/** + * Finds the oldest active wallet that has enough BTC to handle a redemption request. + * @param amount The amount to be redeemed. + * @param bridge The handle to the Bridge on-chain contract. + * @param bitcoinClient Bitcoin client used to interact with the network. + * @param bitcoinNetwork Bitcoin network. + * @returns Promise with the wallet details needed to request a redemption. + */ +export async function findWalletForRedemption( + amount: BigNumber, + bridge: Bridge, + bitcoinClient: BitcoinClient, + bitcoinNetwork: BitcoinNetwork +): Promise<{ + walletPublicKeyHash: string + mainUTXO: UnspentTransactionOutput +}> { + const wallets = await bridge.getNewWalletRegisteredEvents() + + let walletPublicKeyHash + let mainUTXO + let maxAmount = BigNumber.from(0) + for (const wallet of wallets) { + const _walletPublicKeyHash = wallet.walletPublicKeyHash.toPrefixedString() + const { state, mainUtxoHash } = await bridge.wallets(_walletPublicKeyHash) + + // Wallet must be in Live state. + if (state !== WalletState.Live) continue + + const walletBitcoinAddress = encodeToBitcoinAddress( + wallet.walletPublicKeyHash.toString(), + true, + bitcoinNetwork + ) + + const utxos = await bitcoinClient.findAllUnspentTransactionOutputs( + walletBitcoinAddress + ) + + // We need to find correct utxo- utxo components must point to the recent + // main UTXO of the given wallet, as currently known on the chain. + const utxo = utxos.find((utxo) => + mainUtxoHash.equals(bridge.buildUTXOHash(utxo)) + ) + + if (!utxo) continue + + // Save the max possible redemption amount. + maxAmount = utxo.value.gt(maxAmount) ? utxo.value : maxAmount + + if (utxo.value.gte(amount)) { + walletPublicKeyHash = _walletPublicKeyHash + mainUTXO = utxo + + break + } + } + + if (!walletPublicKeyHash || !mainUTXO) + throw new Error( + `Could not find a wallet with enough funds. Maximum redemption amount is ${maxAmount} Satoshi.` + ) + + return { walletPublicKeyHash, mainUTXO } +} diff --git a/typescript/test/utils/mock-bridge.ts b/typescript/test/utils/mock-bridge.ts index d342357c2..cb4e4064a 100644 --- a/typescript/test/utils/mock-bridge.ts +++ b/typescript/test/utils/mock-bridge.ts @@ -15,7 +15,7 @@ import { computeHash160, TransactionHash } from "../../src/bitcoin" import { depositSweepWithNoMainUtxoAndWitnessOutput } from "../data/deposit-sweep" import { Address } from "../../src/ethereum" import { Hex } from "../../src/hex" -import { NewWalletRegisteredEvent } from "../../src/wallet" +import { NewWalletRegisteredEvent, Wallet } from "../../src/wallet" interface DepositSweepProofLogEntry { sweepTx: DecomposedRawTransaction @@ -314,4 +314,21 @@ export class MockBridge implements Bridge { walletRegistry(): Promise { throw new Error("not implemented") } + + wallets(walletPublicKeyHash: string): Promise { + throw new Error("not implemented") + } + + buildUTXOHash(utxo: UnspentTransactionOutput): Hex { + return Hex.from( + utils.solidityKeccak256( + ["bytes32", "uint32", "uint64"], + [ + utxo.transactionHash.reverse().toPrefixedString(), + utxo.outputIndex, + utxo.value, + ] + ) + ) + } } From c64f0b7409b9db0e9711a7cd29763955e5da1e24 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 9 Jun 2023 11:27:59 +0200 Subject: [PATCH 02/23] Update the `findWalletForRedemption` fn Keep the wallet data in object instead of variables and return the `walletPublicKey`(compressed public key of the ECDSA Wallet) instead of the hash(20-byte public key hash of the ECDSA Wallet) because the compressed public key is required in `requestRedemption` fn. --- typescript/src/redemption.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 6256b3057..093d2975d 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -424,17 +424,25 @@ export async function findWalletForRedemption( bitcoinClient: BitcoinClient, bitcoinNetwork: BitcoinNetwork ): Promise<{ - walletPublicKeyHash: string + walletPublicKey: string mainUTXO: UnspentTransactionOutput }> { const wallets = await bridge.getNewWalletRegisteredEvents() - let walletPublicKeyHash - let mainUTXO + let walletData: + | { + walletPublicKey: string + mainUTXO: UnspentTransactionOutput + } + | undefined = undefined let maxAmount = BigNumber.from(0) + for (const wallet of wallets) { - const _walletPublicKeyHash = wallet.walletPublicKeyHash.toPrefixedString() - const { state, mainUtxoHash } = await bridge.wallets(_walletPublicKeyHash) + const prefixedWalletPublicKeyHash = + wallet.walletPublicKeyHash.toPrefixedString() + const { state, mainUtxoHash, walletPublicKey } = await bridge.wallets( + prefixedWalletPublicKeyHash + ) // Wallet must be in Live state. if (state !== WalletState.Live) continue @@ -461,17 +469,19 @@ export async function findWalletForRedemption( maxAmount = utxo.value.gt(maxAmount) ? utxo.value : maxAmount if (utxo.value.gte(amount)) { - walletPublicKeyHash = _walletPublicKeyHash - mainUTXO = utxo + walletData = { + walletPublicKey: walletPublicKey.toString(), + mainUTXO: utxo, + } break } } - if (!walletPublicKeyHash || !mainUTXO) + if (!walletData) throw new Error( `Could not find a wallet with enough funds. Maximum redemption amount is ${maxAmount} Satoshi.` ) - return { walletPublicKeyHash, mainUTXO } + return walletData } From 97d3c65776fb95d95c4e0976004f708865d14f29 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 20 Jun 2023 14:53:10 +0200 Subject: [PATCH 03/23] 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 { From dee70200681af683e84a5e9c6f95d191b0165f36 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 20 Jun 2023 15:01:05 +0200 Subject: [PATCH 04/23] Improve docs in the Ethereum Bridge implementation --- typescript/src/ethereum.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index d64c98a4c..f3d229585 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -698,6 +698,10 @@ export class Bridge }) } + // eslint-disable-next-line valid-jsdoc + /** + * @see {ChainBridge#wallets} + */ async wallets(walletPublicKeyHash: string): Promise { const wallet = await backoffRetrier( this._totalRetryAttempts @@ -708,6 +712,11 @@ export class Bridge return this.parseWalletDetails(wallet) } + /** + * Parses a wallet data using data fetched from the on-chain contract. + * @param wallet Data of the wallet. + * @returns Parsed wallet data. + */ private async parseWalletDetails( wallet: Wallets.WalletStructOutput ): Promise { @@ -730,11 +739,12 @@ export class Bridge } } + // eslint-disable-next-line valid-jsdoc /** * Builds the UTXO hash based on the UTXO components. UTXO hash is computed as * `keccak256(txHash | txOutputIndex | txOutputValue)`. - * @param utxo UTXO components. - * @returns The hash of the UTXO. + * + * @see {ChainBridge#buildUTXOHash} */ buildUTXOHash(utxo: UnspentTransactionOutput): Hex { return Hex.from( From ea80cac0621889d0a98ad475e8508fede5ca2814 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 15:59:42 +0200 Subject: [PATCH 05/23] Rename private fn in Ethereum Bridge handle `toCompressedWalletPublicKey` -> `getWalletCompressedPublicKey` --- typescript/src/ethereum.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index f3d229585..7bc965986 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -644,10 +644,10 @@ export class Bridge const { ecdsaWalletID } = await this.wallets(activeWalletPublicKeyHash) - return (await this.toCompressedWalletPublicKey(ecdsaWalletID)).toString() + return (await this.getWalletCompressedPublicKey(ecdsaWalletID)).toString() } - private async toCompressedWalletPublicKey(ecdsaWalletID: Hex): Promise { + private async getWalletCompressedPublicKey(ecdsaWalletID: Hex): Promise { const walletRegistry = await this.walletRegistry() const uncompressedPublicKey = await walletRegistry.getWalletPublicKey( ecdsaWalletID @@ -724,7 +724,7 @@ export class Bridge return { ecdsaWalletID, - walletPublicKey: await this.toCompressedWalletPublicKey(ecdsaWalletID), + walletPublicKey: await this.getWalletCompressedPublicKey(ecdsaWalletID), mainUtxoHash: Hex.from(wallet.mainUtxoHash), pendingRedemptionsValue: wallet.pendingRedemptionsValue, createdAt: wallet.createdAt, From 8d8982aca20df8e8674370610d7bda815125aea3 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:01:25 +0200 Subject: [PATCH 06/23] Rename fn in Ethereum Bridge handle `buildUTXOHash` -> `buildUtxoHash`. To be consistent with the naming convention we use across the library. --- typescript/src/chain.ts | 2 +- typescript/src/ethereum.ts | 4 ++-- typescript/src/redemption.ts | 2 +- typescript/test/utils/mock-bridge.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/typescript/src/chain.ts b/typescript/src/chain.ts index cb7f2a873..246a23afd 100644 --- a/typescript/src/chain.ts +++ b/typescript/src/chain.ts @@ -259,7 +259,7 @@ export interface Bridge { * @param utxo UTXO components. * @returns The hash of the UTXO. */ - buildUTXOHash(utxo: UnspentTransactionOutput): Hex + buildUtxoHash(utxo: UnspentTransactionOutput): Hex } /** diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index 7bc965986..3cac6a496 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -744,9 +744,9 @@ export class Bridge * Builds the UTXO hash based on the UTXO components. UTXO hash is computed as * `keccak256(txHash | txOutputIndex | txOutputValue)`. * - * @see {ChainBridge#buildUTXOHash} + * @see {ChainBridge#buildUtxoHash} */ - buildUTXOHash(utxo: UnspentTransactionOutput): Hex { + buildUtxoHash(utxo: UnspentTransactionOutput): Hex { return Hex.from( utils.solidityKeccak256( ["bytes32", "uint32", "uint64"], diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 093d2975d..f8307ce49 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -460,7 +460,7 @@ export async function findWalletForRedemption( // We need to find correct utxo- utxo components must point to the recent // main UTXO of the given wallet, as currently known on the chain. const utxo = utxos.find((utxo) => - mainUtxoHash.equals(bridge.buildUTXOHash(utxo)) + mainUtxoHash.equals(bridge.buildUtxoHash(utxo)) ) if (!utxo) continue diff --git a/typescript/test/utils/mock-bridge.ts b/typescript/test/utils/mock-bridge.ts index f9f9a7b36..992d043f2 100644 --- a/typescript/test/utils/mock-bridge.ts +++ b/typescript/test/utils/mock-bridge.ts @@ -351,7 +351,7 @@ export class MockBridge implements Bridge { return wallet! } - buildUTXOHash(utxo: UnspentTransactionOutput): Hex { + buildUtxoHash(utxo: UnspentTransactionOutput): Hex { return Hex.from( utils.solidityKeccak256( ["bytes32", "uint32", "uint64"], From b5a8e47728917a0b599100e214f82bf984676fb4 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:04:01 +0200 Subject: [PATCH 07/23] Update docs for `findWalletForRedemption` fn Clarify in docs that the `amount` param should be in satoshis. --- typescript/src/redemption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index f8307ce49..1c8fd60bf 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -412,7 +412,7 @@ export async function getRedemptionRequest( /** * Finds the oldest active wallet that has enough BTC to handle a redemption request. - * @param amount The amount to be redeemed. + * @param amount The amount to be redeemed in satoshis. * @param bridge The handle to the Bridge on-chain contract. * @param bitcoinClient Bitcoin client used to interact with the network. * @param bitcoinNetwork Bitcoin network. From 9a0adab2396834e71d159b6a580cc1380c98e02d Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:14:37 +0200 Subject: [PATCH 08/23] Update `findWalletForRedemption` fn Rename the field in object returned by the `findWalletForRedemption` fn. `mainUTXO` -> `mainUtxo`. To be consistent with the naming convention we use across the library. --- typescript/src/redemption.ts | 6 +++--- typescript/test/redemption.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 1c8fd60bf..b93d106d4 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -425,14 +425,14 @@ export async function findWalletForRedemption( bitcoinNetwork: BitcoinNetwork ): Promise<{ walletPublicKey: string - mainUTXO: UnspentTransactionOutput + mainUtxo: UnspentTransactionOutput }> { const wallets = await bridge.getNewWalletRegisteredEvents() let walletData: | { walletPublicKey: string - mainUTXO: UnspentTransactionOutput + mainUtxo: UnspentTransactionOutput } | undefined = undefined let maxAmount = BigNumber.from(0) @@ -471,7 +471,7 @@ export async function findWalletForRedemption( if (utxo.value.gte(amount)) { walletData = { walletPublicKey: walletPublicKey.toString(), - mainUTXO: utxo, + mainUtxo: utxo, } break diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 45d2d186c..e9d493d06 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1536,7 +1536,7 @@ describe("Redemption", () => { ] expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUTXO: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.utxos[0], }) }) } From 5b42133eccfc20edd4fd8885554c17109fd48fbb Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:26:44 +0200 Subject: [PATCH 09/23] Refactor the `wallets` fn from `Bridge` interface Accept the wallet public key hash as `Hex` instead of `string` and include the required conversion to prefixed string in the implementation of the Bridge handle. It simplifies the usage of `wallets` method in the `findWalletForRedemption` fn. --- typescript/src/chain.ts | 2 +- typescript/src/ethereum.ts | 10 +++++++--- typescript/src/redemption.ts | 4 +--- typescript/test/utils/mock-bridge.ts | 8 +++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/typescript/src/chain.ts b/typescript/src/chain.ts index 246a23afd..d2240e7c6 100644 --- a/typescript/src/chain.ts +++ b/typescript/src/chain.ts @@ -252,7 +252,7 @@ export interface Bridge { * using Bitcoin HASH160 over the compressed ECDSA public key). * @returns Promise with the wallet details. */ - wallets(walletPublicKeyHash: string): Promise + wallets(walletPublicKeyHash: Hex): Promise /** * Builds the UTXO hash based on the UTXO components. diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index 3cac6a496..1d4e1a562 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -642,7 +642,9 @@ export class Bridge return undefined } - const { ecdsaWalletID } = await this.wallets(activeWalletPublicKeyHash) + const { ecdsaWalletID } = await this.wallets( + Hex.from(activeWalletPublicKeyHash) + ) return (await this.getWalletCompressedPublicKey(ecdsaWalletID)).toString() } @@ -702,11 +704,13 @@ export class Bridge /** * @see {ChainBridge#wallets} */ - async wallets(walletPublicKeyHash: string): Promise { + async wallets(walletPublicKeyHash: Hex): Promise { const wallet = await backoffRetrier( this._totalRetryAttempts )(async () => { - return await this._instance.wallets(walletPublicKeyHash) + return await this._instance.wallets( + walletPublicKeyHash.toPrefixedString() + ) }) return this.parseWalletDetails(wallet) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index b93d106d4..237ca1653 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -438,10 +438,8 @@ export async function findWalletForRedemption( let maxAmount = BigNumber.from(0) for (const wallet of wallets) { - const prefixedWalletPublicKeyHash = - wallet.walletPublicKeyHash.toPrefixedString() const { state, mainUtxoHash, walletPublicKey } = await bridge.wallets( - prefixedWalletPublicKeyHash + wallet.walletPublicKeyHash ) // Wallet must be in Live state. diff --git a/typescript/test/utils/mock-bridge.ts b/typescript/test/utils/mock-bridge.ts index 992d043f2..6a429c655 100644 --- a/typescript/test/utils/mock-bridge.ts +++ b/typescript/test/utils/mock-bridge.ts @@ -345,9 +345,11 @@ export class MockBridge implements Bridge { throw new Error("not implemented") } - async wallets(walletPublicKeyHash: string): Promise { - this._walletsLog.push({ walletPublicKeyHash }) - const wallet = this._wallets.get(walletPublicKeyHash) + async wallets(walletPublicKeyHash: Hex): Promise { + this._walletsLog.push({ + walletPublicKeyHash: walletPublicKeyHash.toPrefixedString(), + }) + const wallet = this._wallets.get(walletPublicKeyHash.toPrefixedString()) return wallet! } From fb6009a4987edf524583cd860db3a96603f5b156 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:33:08 +0200 Subject: [PATCH 10/23] Rename variable in `findWalletForRedemption` fn `utxo` -> `mainUtxo` - for better readability. --- typescript/src/redemption.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 237ca1653..10b0e03b5 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -457,19 +457,19 @@ export async function findWalletForRedemption( // We need to find correct utxo- utxo components must point to the recent // main UTXO of the given wallet, as currently known on the chain. - const utxo = utxos.find((utxo) => + const mainUtxo = utxos.find((utxo) => mainUtxoHash.equals(bridge.buildUtxoHash(utxo)) ) - if (!utxo) continue + if (!mainUtxo) continue // Save the max possible redemption amount. - maxAmount = utxo.value.gt(maxAmount) ? utxo.value : maxAmount + maxAmount = mainUtxo.value.gt(maxAmount) ? mainUtxo.value : maxAmount - if (utxo.value.gte(amount)) { + if (mainUtxo.value.gte(amount)) { walletData = { walletPublicKey: walletPublicKey.toString(), - mainUtxo: utxo, + mainUtxo, } break From 96b1cfefcbf40b98352ac35dd8ce97efa1ae5209 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:43:59 +0200 Subject: [PATCH 11/23] Update `findWalletForRedemption` fn Make sure `mainUtxoHash` is non-zero before we proceed- in that case there is no need to verify this wallet because it cannot handle the redemption request. --- typescript/src/redemption.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 10b0e03b5..5be71f6c0 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -13,6 +13,7 @@ import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" import { WalletState } from "./wallet" import { BitcoinNetwork } from "./bitcoin-network" +import { Hex } from "./hex" /** * Represents a redemption request. @@ -443,7 +444,15 @@ export async function findWalletForRedemption( ) // Wallet must be in Live state. - if (state !== WalletState.Live) continue + if ( + state !== WalletState.Live || + mainUtxoHash.equals( + Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + ) + ) + continue const walletBitcoinAddress = encodeToBitcoinAddress( wallet.walletPublicKeyHash.toString(), From c9209640eecca52008ff13f65c6004c44637e0e1 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jul 2023 16:59:36 +0200 Subject: [PATCH 12/23] Add debug logs to the `findWalletForRedemption` fn Add debug logs for cases when we continue the loop execution to the next wallet. --- typescript/src/redemption.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 5be71f6c0..741f9e893 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -451,8 +451,13 @@ export async function findWalletForRedemption( "0x0000000000000000000000000000000000000000000000000000000000000000" ) ) - ) + ) { + console.debug( + `Wallet is not in Live state or main utxo is not set. \ + Continue the loop execution to the next wallet...` + ) continue + } const walletBitcoinAddress = encodeToBitcoinAddress( wallet.walletPublicKeyHash.toString(), @@ -470,7 +475,14 @@ export async function findWalletForRedemption( mainUtxoHash.equals(bridge.buildUtxoHash(utxo)) ) - if (!mainUtxo) continue + if (!mainUtxo) { + console.debug( + `Could not find matching UTXO on chains for wallet public key hash \ + (${wallet.walletPublicKeyHash.toPrefixedString()}). Continue the loop \ + execution to the next wallet...` + ) + continue + } // Save the max possible redemption amount. maxAmount = mainUtxo.value.gt(maxAmount) ? mainUtxo.value : maxAmount @@ -483,6 +495,12 @@ export async function findWalletForRedemption( break } + + console.debug( + `The wallet (${wallet.walletPublicKeyHash.toPrefixedString()}) \ + cannot handle the redemption request. Continue the loop execution to the \ + next wallet...` + ) } if (!walletData) From 5df505b18eb818c238c22addcb5c4ab91691e863 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 12:35:35 +0200 Subject: [PATCH 13/23] Update `findWalletForRedemption` We need to check if the redemption key is unique- given wallet public key and redeemer output script pair can be used for only one pending request at the same time. Otherwise we should suggest another wallet that can handle a redemption. --- typescript/src/redemption.ts | 20 ++++++++ typescript/test/data/redemption.ts | 10 ++++ typescript/test/redemption.test.ts | 78 ++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 741f9e893..6bdf1de4c 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -414,6 +414,9 @@ export async function getRedemptionRequest( /** * Finds the oldest active wallet that has enough BTC to handle a redemption request. * @param amount The amount to be redeemed in satoshis. + * @param redeemerOutputScript The redeemer output script the redeemed funds + * are supposed to be locked on. Must be un-prefixed and not prepended + * with length. * @param bridge The handle to the Bridge on-chain contract. * @param bitcoinClient Bitcoin client used to interact with the network. * @param bitcoinNetwork Bitcoin network. @@ -421,6 +424,7 @@ export async function getRedemptionRequest( */ export async function findWalletForRedemption( amount: BigNumber, + redeemerOutputScript: string, bridge: Bridge, bitcoinClient: BitcoinClient, bitcoinNetwork: BitcoinNetwork @@ -459,6 +463,22 @@ export async function findWalletForRedemption( continue } + const pendingRedemption = await bridge.pendingRedemptions( + walletPublicKey.toString(), + redeemerOutputScript + ) + + if (pendingRedemption.requestedAt != 0) { + console.debug( + `There is a pending redemption request from this wallet to the + same address. Given wallet public key(${walletPublicKey}) and \ + redeemer output script(${redeemerOutputScript}) pair can be \ + used for only one pending request at the same time. \ + Continue the loop execution to the next wallet...` + ) + continue + } + const walletBitcoinAddress = encodeToBitcoinAddress( wallet.walletPublicKeyHash.toString(), true, diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 0080de3a2..147b0da7d 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -679,6 +679,7 @@ export const findWalletForRedemptionData: { utxos: UnspentTransactionOutput[] } } + pendingRedemption: RedemptionRequest } = { newWalletRegisteredEvents: [ { @@ -786,4 +787,13 @@ export const findWalletForRedemptionData: { ], }, }, + pendingRedemption: { + redeemer: Address.from("0xeb9af8E66869902476347a4eFe59a527a57240ED"), + // script for testnet P2PKH address mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc + redeemerOutputScript: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + requestedAmount: BigNumber.from(1000000), + treasuryFee: BigNumber.from(20000), + txMaxFee: BigNumber.from(20000), + requestedAt: 1688724606, + }, } diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index e9d493d06..3d3664a3b 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1440,6 +1440,8 @@ describe("Redemption", () => { describe("findWalletForRedemption", () => { let bridge: MockBridge let bitcoinClient: MockBitcoinClient + const redeemerOutputScript = + findWalletForRedemptionData.pendingRedemption.redeemerOutputScript context( "when there are no wallets in the network that can hanlde redemption", @@ -1455,6 +1457,7 @@ describe("Redemption", () => { await expect( findWalletForRedemption( amount, + redeemerOutputScript, bridge, bitcoinClient, BitcoinNetwork.Testnet @@ -1501,6 +1504,7 @@ describe("Redemption", () => { beforeEach(async () => { result = await findWalletForRedemption( amount, + redeemerOutputScript, bridge, bitcoinClient, BitcoinNetwork.Testnet @@ -1558,6 +1562,7 @@ describe("Redemption", () => { await expect( findWalletForRedemption( amount, + redeemerOutputScript, bridge, bitcoinClient, BitcoinNetwork.Testnet @@ -1568,6 +1573,79 @@ describe("Redemption", () => { }) } ) + + context( + "when there is pending redemption request from a given wallet to the same address", + () => { + beforeEach(async () => { + const amount: BigNumber = BigNumber.from("1000000") // 0.01 BTC + + const walletPublicKeyHash = + findWalletForRedemptionData.newWalletRegisteredEvents[0] + .walletPublicKeyHash + const pendingRedemptions = new Map< + BigNumberish, + RedemptionRequest + >() + + const key = MockBridge.buildRedemptionKey( + walletPublicKeyHash.toString(), + redeemerOutputScript + ) + + pendingRedemptions.set( + key, + findWalletForRedemptionData.pendingRedemption + ) + bridge.setPendingRedemptions(pendingRedemptions) + + result = await findWalletForRedemption( + amount, + redeemerOutputScript, + 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(2) + expect(bridgeWalletDetailsLogs[0].walletPublicKeyHash).to.eql( + findWalletForRedemptionData.newWalletRegisteredEvents[0].walletPublicKeyHash.toPrefixedString() + ) + expect(bridgeWalletDetailsLogs[1].walletPublicKeyHash).to.eql( + findWalletForRedemptionData.newWalletRegisteredEvents[1].walletPublicKeyHash.toPrefixedString() + ) + }) + + it("should skip the wallet for which there is a pending redemption request to the same redeemer output script and return the wallet data that can handle redemption request", () => { + const expectedWalletPublicKeyHash = + findWalletForRedemptionData.newWalletRegisteredEvents[1] + .walletPublicKeyHash + const expectedWalletData = + findWalletForRedemptionData.wallets[ + expectedWalletPublicKeyHash.toPrefixedString() + ] + + expect(result).to.deep.eq({ + walletPublicKey: expectedWalletData.walletPublicKey.toString(), + mainUtxo: expectedWalletData.utxos[0], + }) + }) + } + ) }) }) }) From 0f1183845e7c29d25d03cdd4b047a740da6561d4 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 13:27:15 +0200 Subject: [PATCH 14/23] Fix debug logs format in `findWalletForRedemption` --- typescript/src/redemption.ts | 42 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 6bdf1de4c..d3138d1ba 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -443,13 +443,22 @@ export async function findWalletForRedemption( let maxAmount = BigNumber.from(0) for (const wallet of wallets) { + const { walletPublicKeyHash } = wallet const { state, mainUtxoHash, walletPublicKey } = await bridge.wallets( - wallet.walletPublicKeyHash + walletPublicKeyHash ) // Wallet must be in Live state. + if (state !== WalletState.Live) { + console.debug( + `Wallet is not in Live state ` + + `(wallet public key hash: ${walletPublicKeyHash.toString()}). ` + + `Continue the loop execution to the next wallet...` + ) + continue + } + if ( - state !== WalletState.Live || mainUtxoHash.equals( Hex.from( "0x0000000000000000000000000000000000000000000000000000000000000000" @@ -457,10 +466,10 @@ export async function findWalletForRedemption( ) ) { console.debug( - `Wallet is not in Live state or main utxo is not set. \ - Continue the loop execution to the next wallet...` + `Main utxo not set for wallet public ` + + `key hash(${walletPublicKeyHash.toString()}). ` + + `Continue the loop execution to the next wallet...` ) - continue } const pendingRedemption = await bridge.pendingRedemptions( @@ -470,11 +479,12 @@ export async function findWalletForRedemption( if (pendingRedemption.requestedAt != 0) { console.debug( - `There is a pending redemption request from this wallet to the - same address. Given wallet public key(${walletPublicKey}) and \ - redeemer output script(${redeemerOutputScript}) pair can be \ - used for only one pending request at the same time. \ - Continue the loop execution to the next wallet...` + `There is a pending redemption request from this wallet to the ` + + `same Bitcoin address. Given wallet public key hash` + + `(${walletPublicKeyHash.toString()}) and redeemer output script ` + + `(${redeemerOutputScript}) pair can be used for only one ` + + `pending request at the same time. ` + + `Continue the loop execution to the next wallet...` ) continue } @@ -497,9 +507,9 @@ export async function findWalletForRedemption( if (!mainUtxo) { console.debug( - `Could not find matching UTXO on chains for wallet public key hash \ - (${wallet.walletPublicKeyHash.toPrefixedString()}). Continue the loop \ - execution to the next wallet...` + `Could not find matching UTXO on chains ` + + `for wallet public key hash(${walletPublicKey.toString()}). ` + + `Continue the loop execution to the next wallet...` ) continue } @@ -517,9 +527,9 @@ export async function findWalletForRedemption( } console.debug( - `The wallet (${wallet.walletPublicKeyHash.toPrefixedString()}) \ - cannot handle the redemption request. Continue the loop execution to the \ - next wallet...` + `The wallet (${walletPublicKeyHash.toString()})` + + `cannot handle the redemption request. ` + + `Continue the loop execution to the next wallet...` ) } From dd1af4548c7e76d2dcb76c3aad8fe0daac795daf Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 13:30:46 +0200 Subject: [PATCH 15/23] Fix typo `hanlde` -> `handle` --- typescript/test/redemption.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 3d3664a3b..beb3d545a 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1444,7 +1444,7 @@ describe("Redemption", () => { findWalletForRedemptionData.pendingRedemption.redeemerOutputScript context( - "when there are no wallets in the network that can hanlde redemption", + "when there are no wallets in the network that can handle redemption", () => { const amount: BigNumber = BigNumber.from("1000000") // 0.01 BTC beforeEach(() => { From 24ede08deb5571f179f870e3bcad6cbfacbccc63 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 22:08:36 +0200 Subject: [PATCH 16/23] Improve `findWalletForRedemption` tests Add some wallets that should be skipped by the wallet selection- E.g. some non-live state, main utxo not set. --- typescript/test/data/redemption.ts | 164 +++++++++++++++++++---------- typescript/test/redemption.test.ts | 94 ++++++++++------- 2 files changed, 162 insertions(+), 96 deletions(-) diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 147b0da7d..0960693d4 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -668,21 +668,51 @@ export const redemptionProof: RedemptionProofTestData = { }, } -export const findWalletForRedemptionData: { - newWalletRegisteredEvents: NewWalletRegisteredEvent[] - wallets: { - [walletPublicKeyHash: string]: { - state: WalletState - mainUtxoHash: Hex - walletPublicKey: Hex - btcAddress: string - utxos: UnspentTransactionOutput[] - } +interface FindWalletForRedemptionWaleltData { + data: { + state: WalletState + mainUtxoHash: Hex + walletPublicKey: Hex + btcAddress: string + utxos: UnspentTransactionOutput[] + } + event: { + blockNumber: number + blockHash: Hex + transactionHash: Hex + ecdsaWalletID: Hex + walletPublicKeyHash: Hex } +} + +export const findWalletForRedemptionData: { + liveWallet: FindWalletForRedemptionWaleltData + walletWithoutUtxo: FindWalletForRedemptionWaleltData + nonLiveWallet: FindWalletForRedemptionWaleltData + walletWithPendingRedemption: FindWalletForRedemptionWaleltData pendingRedemption: RedemptionRequest } = { - newWalletRegisteredEvents: [ - { + liveWallet: { + data: { + state: WalletState.Live, + mainUtxoHash: Hex.from( + "0x3ded9dcfce0ffe479640013ebeeb69b6a82306004f9525b1346ca3b553efc6aa" + ), + walletPublicKey: Hex.from( + "0x028ed84936be6a9f594a2dcc636d4bebf132713da3ce4dac5c61afbf8bbb47d6f7" + ), + btcAddress: "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja", + utxos: [ + { + transactionHash: Hex.from( + "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" + ), + outputIndex: 0, + value: BigNumber.from("791613461"), + }, + ], + }, + event: { blockNumber: 8367602, blockHash: Hex.from( "0x908ea9c82b388a760e6dd070522e5421d88b8931fbac6702119f9e9a483dd022" @@ -697,77 +727,84 @@ export const findWalletForRedemptionData: { "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e" ), }, - { - blockNumber: 8502240, - blockHash: Hex.from( - "0x4baab7520cf79a05f22723688bcd1f2805778829aa4362250b8ee702f34f4daf" - ), - transactionHash: Hex.from( - "0xe88761c7203335e237366ec2ffca1e7cf2690eab343ad700e6a6e6dc236638b1" - ), - ecdsaWalletID: Hex.from( - "0x0c70f262eaff2cdaaddb5a5e4ecfdda6edad7f1789954ad287bfa7e594173c64" + }, + + walletWithoutUtxo: { + data: { + state: WalletState.Live, + mainUtxoHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" ), - walletPublicKeyHash: Hex.from( - "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed" + walletPublicKey: Hex.from( + "0x030fbbae74e6d85342819e719575949a1349e975b69fb382e9fef671a3a74efc52" ), + btcAddress: "tb1qkct7r24k4wutnsun84rvp3qsyt8yfpvqz89d2y", + utxos: [ + { + transactionHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + outputIndex: 0, + value: BigNumber.from("0"), + }, + ], }, - { - blockNumber: 8981644, + event: { + blockNumber: 9103428, blockHash: Hex.from( - "0x6681b1bb168fb86755c2a796169cb0e06949caac9fc7145d527d94d5209a64ad" + "0x92ad328db2cb1d2aad60ac809660e05e2b6763ddd376ca21630e304c98f23600" ), transactionHash: Hex.from( - "0xea3a8853c658145c95165d7847152aeedc3ff29406ec263abfc9b1436402b7b7" + "0x309085ebb92e10eb9e665c7d90c94e053f13b36f9bc8017e820bc879ba629b8e" ), ecdsaWalletID: Hex.from( - "0x7a1437d67f49adfd44e03ddc85be0f6988715d7c39dfb0ca9780f1a88bcdca25" + "0xd27bfaaad9c3489e613eb3664c3b9958bd9a494377123689733267cb4a5767ba" ), walletPublicKeyHash: Hex.from( - "0x328d992e5f5b71de51a1b40fcc4056b99a88a647" + "0xb617e1aab6abb8b9c3933d46c0c41022ce448580" ), }, - ], - wallets: { - "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e": { - state: WalletState.Live, + }, + + nonLiveWallet: { + data: { + state: WalletState.Unknown, mainUtxoHash: Hex.from( - "0x3ded9dcfce0ffe479640013ebeeb69b6a82306004f9525b1346ca3b553efc6aa" + "0x0000000000000000000000000000000000000000000000000000000000000000" ), walletPublicKey: Hex.from( - "0x028ed84936be6a9f594a2dcc636d4bebf132713da3ce4dac5c61afbf8bbb47d6f7" + "0x02633b102417009ae55103798f4d366dfccb081dcf20025088b9bf10a8e15d8ded" ), - btcAddress: "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja", + btcAddress: "tb1qf6jvyd680ncf9dtr5znha9ql5jmw84lupwwuf6", utxos: [ { transactionHash: Hex.from( - "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" + "0x0000000000000000000000000000000000000000000000000000000000000000" ), outputIndex: 0, - value: BigNumber.from("791613461"), + value: BigNumber.from("0"), }, ], }, - "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed": { - state: WalletState.Live, - mainUtxoHash: Hex.from( - "0x3ea242dd8a7f7f7abd548ca6590de70a1e992cbd6e4ae18b7a91c9b899067626" + event: { + blockNumber: 9171960, + blockHash: Hex.from( + "0xe9a404b724183cb8f77e45718b365051e8d5ccc4a72dfece30af7596eeee4748" ), - walletPublicKey: Hex.from( - "0x025183c15164e1b2211eb359fce2ceeefc3abad3af6d760cc6355f9de99bf60229" + transactionHash: Hex.from( + "0x867f2c985cbe44f92a7ca2c14268b9ae78275e1c339692e1847548725484e72d" + ), + ecdsaWalletID: Hex.from( + "0x96975ddd76bbb2e15ed4498de6d92187ec5282913b3af3891a4e2d60581a8787" + ), + walletPublicKeyHash: Hex.from( + "0x4ea4c237477cf092b563a0a77e941fa4b6e3d7fc" ), - btcAddress: "tb1qwecrg07qpnxz6rxk2dswdt2qq6t75rldweydm2", - utxos: [ - { - transactionHash: Hex.from( - "0xda0e364abb3ed952bcc694e48bbcff19131ba9513fe981b303fa900cff0f9fbc" - ), - outputIndex: 0, - value: BigNumber.from("164380000"), - }, - ], }, - "0x328d992e5f5b71de51a1b40fcc4056b99a88a647": { + }, + + walletWithPendingRedemption: { + data: { state: WalletState.Live, mainUtxoHash: Hex.from( "0xb3024ef698084cfdfba459338864a595d31081748b28aa5eb02312671a720531" @@ -786,6 +823,21 @@ export const findWalletForRedemptionData: { }, ], }, + event: { + blockNumber: 8981644, + blockHash: Hex.from( + "0x6681b1bb168fb86755c2a796169cb0e06949caac9fc7145d527d94d5209a64ad" + ), + transactionHash: Hex.from( + "0xea3a8853c658145c95165d7847152aeedc3ff29406ec263abfc9b1436402b7b7" + ), + ecdsaWalletID: Hex.from( + "0x7a1437d67f49adfd44e03ddc85be0f6988715d7c39dfb0ca9780f1a88bcdca25" + ), + walletPublicKeyHash: Hex.from( + "0x328d992e5f5b71de51a1b40fcc4056b99a88a647" + ), + }, }, pendingRedemption: { redeemer: Address.from("0xeb9af8E66869902476347a4eFe59a527a57240ED"), diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index beb3d545a..4f75ef490 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1440,8 +1440,10 @@ describe("Redemption", () => { describe("findWalletForRedemption", () => { let bridge: MockBridge let bitcoinClient: MockBitcoinClient + // script for testnet P2WSH address + // tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv const redeemerOutputScript = - findWalletForRedemptionData.pendingRedemption.redeemerOutputScript + "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c" context( "when there are no wallets in the network that can handle redemption", @@ -1471,27 +1473,40 @@ describe("Redemption", () => { context("when there are registered wallets in the network", () => { let result: Awaited | never> + const walletsOrder = [ + findWalletForRedemptionData.nonLiveWallet, + findWalletForRedemptionData.walletWithoutUtxo, + findWalletForRedemptionData.walletWithPendingRedemption, + findWalletForRedemptionData.liveWallet, + ] beforeEach(async () => { bitcoinClient = new MockBitcoinClient() bridge = new MockBridge() - bridge.newWalletRegisteredEvents = - findWalletForRedemptionData.newWalletRegisteredEvents + + bridge.newWalletRegisteredEvents = walletsOrder.map( + (wallet) => wallet.event + ) + 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) + + walletsOrder.forEach((wallet) => { + const { state, mainUtxoHash, walletPublicKey, btcAddress, utxos } = + wallet.data + walletsUnspentTransacionOutputs.set(btcAddress, utxos) - } + bridge.setWallet( + wallet.event.walletPublicKeyHash.toPrefixedString(), + { + state, + mainUtxoHash, + walletPublicKey, + } as Wallet + ) + }) bitcoinClient.unspentTransactionOutputs = walletsUnspentTransacionOutputs @@ -1524,20 +1539,23 @@ describe("Redemption", () => { 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() - ) + const wallets = Array.from(walletsOrder) + // Remove last live wallet. + wallets.pop() + + expect(bridgeWalletDetailsLogs.length).to.eql(wallets.length) + + wallets.forEach((wallet, index) => { + expect(bridgeWalletDetailsLogs[index].walletPublicKeyHash).to.eql( + wallet.event.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() - ] + findWalletForRedemptionData.walletWithPendingRedemption.data + expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), mainUtxo: expectedWalletData.utxos[0], @@ -1550,9 +1568,8 @@ describe("Redemption", () => { "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 - ) + const expectedMaxAmount = walletsOrder + .map((wallet) => wallet.data) .map((wallet) => wallet.utxos) .flat() .map((utxo) => utxo.value) @@ -1578,11 +1595,14 @@ describe("Redemption", () => { "when there is pending redemption request from a given wallet to the same address", () => { beforeEach(async () => { + const redeemerOutputScript = + findWalletForRedemptionData.pendingRedemption.redeemerOutputScript const amount: BigNumber = BigNumber.from("1000000") // 0.01 BTC const walletPublicKeyHash = - findWalletForRedemptionData.newWalletRegisteredEvents[0] + findWalletForRedemptionData.walletWithPendingRedemption.event .walletPublicKeyHash + const pendingRedemptions = new Map< BigNumberish, RedemptionRequest @@ -1621,23 +1641,17 @@ describe("Redemption", () => { it("should get wallet data details", () => { const bridgeWalletDetailsLogs = bridge.walletsLog - expect(bridgeWalletDetailsLogs.length).to.eql(2) - expect(bridgeWalletDetailsLogs[0].walletPublicKeyHash).to.eql( - findWalletForRedemptionData.newWalletRegisteredEvents[0].walletPublicKeyHash.toPrefixedString() - ) - expect(bridgeWalletDetailsLogs[1].walletPublicKeyHash).to.eql( - findWalletForRedemptionData.newWalletRegisteredEvents[1].walletPublicKeyHash.toPrefixedString() - ) + expect(bridgeWalletDetailsLogs.length).to.eql(walletsOrder.length) + walletsOrder.forEach((wallet, index) => { + expect(bridgeWalletDetailsLogs[index].walletPublicKeyHash).to.eql( + wallet.event.walletPublicKeyHash.toPrefixedString() + ) + }) }) it("should skip the wallet for which there is a pending redemption request to the same redeemer output script and return the wallet data that can handle redemption request", () => { - const expectedWalletPublicKeyHash = - findWalletForRedemptionData.newWalletRegisteredEvents[1] - .walletPublicKeyHash const expectedWalletData = - findWalletForRedemptionData.wallets[ - expectedWalletPublicKeyHash.toPrefixedString() - ] + findWalletForRedemptionData.liveWallet.data expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), From 159ae96292ceae70e0b271352ceb72a2266a4419 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 22:10:32 +0200 Subject: [PATCH 17/23] Update logs in `findWalletForRedemption` Log wallet public key hash instead of wallet public key- for consistency. --- typescript/src/redemption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index d3138d1ba..5bce57ec4 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -508,7 +508,7 @@ export async function findWalletForRedemption( if (!mainUtxo) { console.debug( `Could not find matching UTXO on chains ` + - `for wallet public key hash(${walletPublicKey.toString()}). ` + + `for wallet public key hash(${walletPublicKeyHash.toString()}). ` + `Continue the loop execution to the next wallet...` ) continue From ca2191acaf102b0b4d24a9bc80c481f3c3df2eb9 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 22:18:08 +0200 Subject: [PATCH 18/23] Reorder arguments in `findWalletForRedemption` To be consistent with other functions- pass handles at the end. --- typescript/src/redemption.ts | 4 ++-- typescript/test/redemption.test.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 5bce57ec4..6f728f708 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -417,17 +417,17 @@ export async function getRedemptionRequest( * @param redeemerOutputScript The redeemer output script the redeemed funds * are supposed to be locked on. Must be un-prefixed and not prepended * with length. + * @param bitcoinNetwork Bitcoin network. * @param bridge The handle to the Bridge on-chain contract. * @param bitcoinClient Bitcoin client used to interact with the network. - * @param bitcoinNetwork Bitcoin network. * @returns Promise with the wallet details needed to request a redemption. */ export async function findWalletForRedemption( amount: BigNumber, redeemerOutputScript: string, + bitcoinNetwork: BitcoinNetwork, bridge: Bridge, bitcoinClient: BitcoinClient, - bitcoinNetwork: BitcoinNetwork ): Promise<{ walletPublicKey: string mainUtxo: UnspentTransactionOutput diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 4f75ef490..986582958 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1460,9 +1460,9 @@ describe("Redemption", () => { findWalletForRedemption( amount, redeemerOutputScript, + BitcoinNetwork.Testnet, bridge, - bitcoinClient, - BitcoinNetwork.Testnet + bitcoinClient ) ).to.be.rejectedWith( "Could not find a wallet with enough funds. Maximum redemption amount is 0 Satoshi." @@ -1520,9 +1520,9 @@ describe("Redemption", () => { result = await findWalletForRedemption( amount, redeemerOutputScript, + BitcoinNetwork.Testnet, bridge, bitcoinClient, - BitcoinNetwork.Testnet ) }) @@ -1580,9 +1580,9 @@ describe("Redemption", () => { findWalletForRedemption( amount, redeemerOutputScript, + BitcoinNetwork.Testnet, bridge, bitcoinClient, - BitcoinNetwork.Testnet ) ).to.be.rejectedWith( `Could not find a wallet with enough funds. Maximum redemption amount is ${expectedMaxAmount.toString()} Satoshi.` @@ -1622,9 +1622,9 @@ describe("Redemption", () => { result = await findWalletForRedemption( amount, redeemerOutputScript, + BitcoinNetwork.Testnet, bridge, bitcoinClient, - BitcoinNetwork.Testnet ) }) From d6e86272aac0603278ee6d218e486b697392f88f Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 22:23:08 +0200 Subject: [PATCH 19/23] Update `activeWalletPublicKey` fn in Eth Bridge We already resolve the compressed public key in wallets function call under `parseWalletDetails`- so there is no need to call `getWalletCompressedPublicKey` again. --- typescript/src/ethereum.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/src/ethereum.ts b/typescript/src/ethereum.ts index 1d4e1a562..ac97da27c 100644 --- a/typescript/src/ethereum.ts +++ b/typescript/src/ethereum.ts @@ -642,11 +642,11 @@ export class Bridge return undefined } - const { ecdsaWalletID } = await this.wallets( + const { walletPublicKey } = await this.wallets( Hex.from(activeWalletPublicKeyHash) ) - return (await this.getWalletCompressedPublicKey(ecdsaWalletID)).toString() + return walletPublicKey.toString() } private async getWalletCompressedPublicKey(ecdsaWalletID: Hex): Promise { From 921349ff07db0effe9bc658afab6961ff262f35f Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 22:41:47 +0200 Subject: [PATCH 20/23] Fix formatting errors --- typescript/src/redemption.ts | 2 +- typescript/test/data/redemption.ts | 2 +- typescript/test/redemption.test.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 6f728f708..c431604cf 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -427,7 +427,7 @@ export async function findWalletForRedemption( redeemerOutputScript: string, bitcoinNetwork: BitcoinNetwork, bridge: Bridge, - bitcoinClient: BitcoinClient, + bitcoinClient: BitcoinClient ): Promise<{ walletPublicKey: string mainUtxo: UnspentTransactionOutput diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 0960693d4..7724008fb 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -11,7 +11,7 @@ import { import { RedemptionRequest } from "../../src/redemption" import { Address } from "../../src/ethereum" import { Hex } from "../../src" -import { NewWalletRegisteredEvent, WalletState } from "../../src/wallet" +import { WalletState } from "../../src/wallet" /** * Private key (testnet) of the wallet. diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 986582958..e2c8cc4b9 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1522,7 +1522,7 @@ describe("Redemption", () => { redeemerOutputScript, BitcoinNetwork.Testnet, bridge, - bitcoinClient, + bitcoinClient ) }) @@ -1582,7 +1582,7 @@ describe("Redemption", () => { redeemerOutputScript, BitcoinNetwork.Testnet, bridge, - bitcoinClient, + bitcoinClient ) ).to.be.rejectedWith( `Could not find a wallet with enough funds. Maximum redemption amount is ${expectedMaxAmount.toString()} Satoshi.` @@ -1624,7 +1624,7 @@ describe("Redemption", () => { redeemerOutputScript, BitcoinNetwork.Testnet, bridge, - bitcoinClient, + bitcoinClient ) }) From b86e251934c71f25dd5b6fca3b9575bbf44beac1 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 22:43:38 +0200 Subject: [PATCH 21/23] Add commit hash to the `.git-blame-ignore-revs` --- .git-blame-ignore-revs | 1 + 1 file changed, 1 insertion(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f85e8bf80..bad52e692 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -11,6 +11,7 @@ e032bba7ce52a877a77573fb5a14944623c77d02 7ad277ff8369b201515ba22872c1251ec93a6b81 5c7e2e3620ec06fb9a7c358c541d491da977ec08 3d8c4861ac467f0390733916775a9ccfafe752e3 +921349ff07db0effe9bc658afab6961ff262f35f # s/btc/BTC da039720b6eb36e5f7102e83a3e2bb95a09b5772 \ No newline at end of file From 9a0f289ac279b3372addcf75d9edb51db6bb5b61 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 10 Jul 2023 11:39:28 +0200 Subject: [PATCH 22/23] Update `findWalletForRedemption` function Take into account pending redemptions value- say Say the wallet has `100 BTC`, redemptions are processed every 3 hours, and so far, requests for `93 BTC` has been registered in the Bridge. Based on the previous version of the code, that wallet would still be selected for all redemptions `<= 100 BTC` and the transaction to Ethereum requesting redemption higher than `7 BTC` would fail. This commit fixes that issue. --- typescript/src/redemption.ts | 11 +++---- typescript/test/data/redemption.ts | 9 ++++-- typescript/test/redemption.test.ts | 46 ++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index c431604cf..013dbeed8 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -444,9 +444,8 @@ export async function findWalletForRedemption( for (const wallet of wallets) { const { walletPublicKeyHash } = wallet - const { state, mainUtxoHash, walletPublicKey } = await bridge.wallets( - walletPublicKeyHash - ) + const { state, mainUtxoHash, walletPublicKey, pendingRedemptionsValue } = + await bridge.wallets(walletPublicKeyHash) // Wallet must be in Live state. if (state !== WalletState.Live) { @@ -514,10 +513,12 @@ export async function findWalletForRedemption( continue } + const walletBTCBalance = mainUtxo.value.sub(pendingRedemptionsValue) + // Save the max possible redemption amount. - maxAmount = mainUtxo.value.gt(maxAmount) ? mainUtxo.value : maxAmount + maxAmount = walletBTCBalance.gt(maxAmount) ? walletBTCBalance : maxAmount - if (mainUtxo.value.gte(amount)) { + if (walletBTCBalance.gte(amount)) { walletData = { walletPublicKey: walletPublicKey.toString(), mainUtxo, diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 7724008fb..285169c11 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -675,6 +675,7 @@ interface FindWalletForRedemptionWaleltData { walletPublicKey: Hex btcAddress: string utxos: UnspentTransactionOutput[] + pendingRedemptionsValue: BigNumber } event: { blockNumber: number @@ -711,6 +712,7 @@ export const findWalletForRedemptionData: { value: BigNumber.from("791613461"), }, ], + pendingRedemptionsValue: BigNumber.from(0), }, event: { blockNumber: 8367602, @@ -728,7 +730,6 @@ export const findWalletForRedemptionData: { ), }, }, - walletWithoutUtxo: { data: { state: WalletState.Live, @@ -748,6 +749,7 @@ export const findWalletForRedemptionData: { value: BigNumber.from("0"), }, ], + pendingRedemptionsValue: BigNumber.from(0), }, event: { blockNumber: 9103428, @@ -785,6 +787,7 @@ export const findWalletForRedemptionData: { value: BigNumber.from("0"), }, ], + pendingRedemptionsValue: BigNumber.from(0), }, event: { blockNumber: 9171960, @@ -802,7 +805,6 @@ export const findWalletForRedemptionData: { ), }, }, - walletWithPendingRedemption: { data: { state: WalletState.Live, @@ -819,9 +821,10 @@ export const findWalletForRedemptionData: { "0x81c4884a8c2fccbeb57745a5e59f895a9c1bb8fc42eecc82269100a1a46bbb85" ), outputIndex: 0, - value: BigNumber.from("3370000"), + value: BigNumber.from("3370000"), // 0.0337 BTC }, ], + pendingRedemptionsValue: BigNumber.from(2370000), // 0.0237 BTC }, event: { blockNumber: 8981644, diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index e2c8cc4b9..26c58ddc7 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1494,8 +1494,14 @@ describe("Redemption", () => { >() walletsOrder.forEach((wallet) => { - const { state, mainUtxoHash, walletPublicKey, btcAddress, utxos } = - wallet.data + const { + state, + mainUtxoHash, + walletPublicKey, + btcAddress, + utxos, + pendingRedemptionsValue, + } = wallet.data walletsUnspentTransacionOutputs.set(btcAddress, utxos) bridge.setWallet( @@ -1504,6 +1510,7 @@ describe("Redemption", () => { state, mainUtxoHash, walletPublicKey, + pendingRedemptionsValue, } as Wallet ) }) @@ -1660,6 +1667,41 @@ describe("Redemption", () => { }) } ) + + context( + "when wallet has pending redemptions and the requested amount is greater than possible", + () => { + beforeEach(async () => { + const wallet = + findWalletForRedemptionData.walletWithPendingRedemption + const walletBTCBalance = wallet.data.utxos[0].value + + const amount: BigNumber = walletBTCBalance + .sub(wallet.data.pendingRedemptionsValue) + .add(BigNumber.from(500000)) // 0.005 BTC + + console.log("amount", amount.toString()) + + result = await findWalletForRedemption( + amount, + redeemerOutputScript, + BitcoinNetwork.Testnet, + bridge, + bitcoinClient + ) + }) + + it("should skip the wallet wallet with pending redemptions and return the wallet data that can handle redemption request ", () => { + const expectedWalletData = + findWalletForRedemptionData.liveWallet.data + + expect(result).to.deep.eq({ + walletPublicKey: expectedWalletData.walletPublicKey.toString(), + mainUtxo: expectedWalletData.utxos[0], + }) + }) + } + ) }) }) }) From e68a7efadd8bced671901e82a8a2bf327f4b3f3c Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 10 Jul 2023 13:01:32 +0200 Subject: [PATCH 23/23] Leave TODO in `findWalletForRedemption` function In case a wallet is working on something (e.g. redemption) and a Bitcoin transaction was already submitted by the wallet to the bitcoin chain (new utxo returned from bitcoin client), but proof hasn't been submitted yet to the Bridge (old main utxo returned from the Bridge) the `findWalletForRedemption` function will not find such a wallet. To cover this case, we should take, for example, the last 5 transactions made by the wallet into account. We will address this issue in a follow-up work. --- typescript/src/redemption.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 91d169d37..05a2a6dda 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -495,6 +495,13 @@ export async function findWalletForRedemption( bitcoinNetwork ) + // TODO: In case a wallet is working on something (e.g. redemption) and a + // Bitcoin transaction was already submitted by the wallet to the bitcoin + // chain (new utxo returned from bitcoin client), but proof hasn't been + // submitted yet to the Bridge (old main utxo returned from the Bridge) the + // `findWalletForRedemption` function will not find such a wallet. To cover + // this case, we should take, for example, the last 5 transactions made by + // the wallet into account. We will address this issue in a follow-up work. const utxos = await bitcoinClient.findAllUnspentTransactionOutputs( walletBitcoinAddress )