From 751e81fa21af596462e84c8d0d3bab6d0f951ae6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 7 Jun 2023 14:42:20 +0200 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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 ) From 97d56b3fdc0fefb294d141d674a0a13aadce247c Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 11 Jul 2023 13:43:20 +0200 Subject: [PATCH 24/30] Expose `getTransactionHistory` function Here we expose a function allowing to get the transaction history for given Bitcoin address. --- typescript/src/bitcoin.ts | 12 +++++ typescript/src/electrum.ts | 52 ++++++++++++++++++++ typescript/test/electrum.test.ts | 22 +++++++++ typescript/test/utils/mock-bitcoin-client.ts | 24 +++++++++ 4 files changed, 110 insertions(+) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index bb330be3f..735c3dc94 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -333,6 +333,18 @@ export interface Client { address: string ): Promise + /** + * Gets the history of confirmed transactions for given Bitcoin address. + * Returned transactions are sorted from oldest to newest. The returned + * result does not contain unconfirmed transactions living in the mempool + * at the moment of request. + * @param address - Bitcoin address transaction history should be determined for. + * @param limit - Optional parameter that can limit the resulting list to + * a specific number of last transaction. For example, limit = 5 will + * return only the last 5 transactions for the given address. + */ + getTransactionHistory(address: string, limit?: number): Promise + /** * Gets the full transaction object for given transaction hash. * @param transactionHash - Hash of the transaction. diff --git a/typescript/src/electrum.ts b/typescript/src/electrum.ts index a43993209..d467f42f7 100644 --- a/typescript/src/electrum.ts +++ b/typescript/src/electrum.ts @@ -253,6 +253,58 @@ export class Client implements BitcoinClient { ) } + // eslint-disable-next-line valid-jsdoc + /** + * @see {BitcoinClient#getTransactionHistory} + */ + getTransactionHistory( + address: string, + limit?: number + ): Promise { + return this.withElectrum(async (electrum: Electrum) => { + const script = bcoin.Script.fromAddress(address).toRaw().toString("hex") + + // eslint-disable-next-line camelcase + type HistoryItem = { height: number; tx_hash: string } + + let historyItems: HistoryItem[] = await this.withBackoffRetrier< + HistoryItem[] + >()(async () => { + return await electrum.blockchain_scripthash_getHistory( + computeScriptHash(script) + ) + }) + + // According to https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history + // unconfirmed items living in the mempool are appended at the end of the + // returned list and their height value is either -1 or 0. That means + // we need to take all items with height >0 to obtain a confirmed txs + // history. + historyItems = historyItems.filter((item) => item.height > 0) + + // The list returned from blockchain.scripthash.get_history is sorted by + // the block height in the ascending order though we are sorting it + // again just in case (e.g. API contract changes). + historyItems = historyItems.sort( + (item1, item2) => item1.height - item2.height + ) + + if ( + typeof limit !== "undefined" && + limit > 0 && + historyItems.length > limit + ) { + historyItems = historyItems.slice(-limit) + } + + const transactions = historyItems.map((item) => + this.getTransaction(TransactionHash.from(item.tx_hash)) + ) + + return Promise.all(transactions) + }) + } + // eslint-disable-next-line valid-jsdoc /** * @see {BitcoinClient#getTransaction} diff --git a/typescript/test/electrum.test.ts b/typescript/test/electrum.test.ts index c500a7efa..09fca2c2d 100644 --- a/typescript/test/electrum.test.ts +++ b/typescript/test/electrum.test.ts @@ -108,6 +108,28 @@ describe("Electrum", () => { }) }) + describe("getTransactionHistory", () => { + it("should return proper transaction history for the given address", async () => { + // https://live.blockcypher.com/btc-testnet/address/tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx + const transactions = await electrumClient.getTransactionHistory( + "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", + 5 + ) + + const transactionsHashes = transactions.map((t) => + t.transactionHash.toString() + ) + + expect(transactionsHashes).to.be.eql([ + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1", + "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e", + "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6", + "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6", + "605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44", + ]) + }) + }) + describe("getTransaction", () => { it("should return proper transaction for the given hash", async () => { const result = await electrumClient.getTransaction( diff --git a/typescript/test/utils/mock-bitcoin-client.ts b/typescript/test/utils/mock-bitcoin-client.ts index 95e4305b1..b37dd7820 100644 --- a/typescript/test/utils/mock-bitcoin-client.ts +++ b/typescript/test/utils/mock-bitcoin-client.ts @@ -27,6 +27,7 @@ export class MockBitcoinClient implements Client { position: 0, } private _broadcastLog: RawTransaction[] = [] + private _transactionHistory = new Map() set unspentTransactionOutputs( value: Map @@ -58,6 +59,10 @@ export class MockBitcoinClient implements Client { this._transactionMerkle = value } + set transactionHistory(value: Map) { + this._transactionHistory = value + } + get broadcastLog(): RawTransaction[] { return this._broadcastLog } @@ -80,6 +85,25 @@ export class MockBitcoinClient implements Client { }) } + getTransactionHistory( + address: string, + limit?: number + ): Promise { + return new Promise((resolve, _) => { + let transactions = this._transactionHistory.get(address) as Transaction[] + + if ( + typeof limit !== "undefined" && + limit > 0 && + transactions.length > limit + ) { + transactions = transactions.slice(-limit) + } + + resolve(transactions) + }) + } + getTransaction(transactionHash: TransactionHash): Promise { return new Promise((resolve, _) => { resolve(this._transactions.get(transactionHash.toString()) as Transaction) From 499467257a3214aacf69e586a3cb412e7bd44fd3 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 11 Jul 2023 17:43:16 +0200 Subject: [PATCH 25/30] Expose `determineWalletMainUtxo` function Here we expose a function allowing to determine the wallet main UTXO based on the wallet recent transactions history and the main UTXO hash stored on the Bridge contract. --- typescript/src/wallet.ts | 124 ++++++++++++++++- typescript/test/wallet.test.ts | 246 +++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 typescript/test/wallet.test.ts diff --git a/typescript/src/wallet.ts b/typescript/src/wallet.ts index 2a0e0298b..2d1bccbf3 100644 --- a/typescript/src/wallet.ts +++ b/typescript/src/wallet.ts @@ -1,6 +1,14 @@ import { BigNumber } from "ethers" import { Hex } from "./hex" -import { Event, Identifier } from "./chain" +import { Bridge, Event, Identifier } from "./chain" +import { + Client as BitcoinClient, + createOutputScriptFromAddress, + encodeToBitcoinAddress, + TransactionOutput, + UnspentTransactionOutput, +} from "./bitcoin" +import { BitcoinNetwork } from "./bitcoin-network" /* eslint-disable no-unused-vars */ export enum WalletState { @@ -209,3 +217,117 @@ type DkgResult = { */ membersHash: Hex } + +/** + * Determines the plain-text wallet main UTXO currently registered in the + * Bridge on-chain contract. The returned main UTXO can be undefined if the + * wallet does not have a main UTXO registered in the Bridge at the moment. + * + * WARNING: THIS FUNCTION CANNOT DETERMINE THE MAIN UTXO IF IT COMES FROM A + * BITCOIN TRANSACTION THAT IS NOT ONE OF THE LATEST FIVE TRANSACTIONS + * TARGETING THE GIVEN WALLET PUBLIC KEY HASH. HOWEVER, SUCH A CASE IS + * VERY UNLIKELY. + * + * @param walletPublicKeyHash - Public key hash of the wallet. + * @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 holding the wallet main UTXO or undefined value. + */ +export async function determineWalletMainUtxo( + walletPublicKeyHash: Hex, + bridge: Bridge, + bitcoinClient: BitcoinClient, + bitcoinNetwork: BitcoinNetwork +): Promise { + const { mainUtxoHash } = await bridge.wallets(walletPublicKeyHash) + + // Valid case when the wallet doesn't have a main UTXO registered into + // the Bridge. + if ( + mainUtxoHash.equals( + Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + ) + ) { + return undefined + } + + // Declare a helper function that will try to determine the main UTXO for + // the given wallet address type. + const determine = async ( + witnessAddress: boolean + ): Promise => { + // Build the wallet Bitcoin address based on its public key hash. + const walletAddress = encodeToBitcoinAddress( + walletPublicKeyHash.toString(), + witnessAddress, + bitcoinNetwork + ) + + // Get the wallet transaction history. The wallet main UTXO registered in the + // Bridge almost always comes from the latest BTC transaction made by the wallet. + // However, there may be cases where the BTC transaction was made but their + // SPV proof is not yet submitted to the Bridge thus the registered main UTXO + // points to the second last BTC transaction. In theory, such a gap between + // the actual latest BTC transaction and the registered main UTXO in the + // Bridge may be even wider. The exact behavior is a wallet implementation + // detail and not a protocol invariant so, it may be subject of changes. + // To cover the worst possible cases, we always take the five latest + // transactions made by the wallet for consideration. + const walletTransactions = await bitcoinClient.getTransactionHistory( + walletAddress, + 5 + ) + + // Get the wallet script based on the wallet address. This is required + // to find transaction outputs that lock funds on the wallet. + const walletScript = createOutputScriptFromAddress(walletAddress) + const isWalletOutput = (output: TransactionOutput) => + walletScript.equals(output.scriptPubKey) + + // Start iterating from the latest transaction as the chance it matches + // the wallet main UTXO is the highest. + for (let i = walletTransactions.length - 1; i >= 0; i--) { + const walletTransaction = walletTransactions[i] + + // Find the output that locks the funds on the wallet. Only such an output + // can be a wallet main UTXO. + const outputIndex = walletTransaction.outputs.findIndex(isWalletOutput) + + // Should never happen as all transactions come from wallet history. Just + // in case check whether the wallet output was actually found. + if (outputIndex < 0) { + continue + } + + // Build a candidate UTXO instance based on the detected output. + const utxo: UnspentTransactionOutput = { + transactionHash: walletTransaction.transactionHash, + outputIndex: outputIndex, + value: walletTransaction.outputs[outputIndex].value, + } + + // Check whether the candidate UTXO hash matches the main UTXO hash stored + // on the Bridge. + if (mainUtxoHash.equals(bridge.buildUtxoHash(utxo))) { + return utxo + } + } + + return undefined + } + + // The most common case is that the wallet uses a witness address for all + // operations. Try to determine the main UTXO for that address first as the + // chance for success is the highest here. + const mainUtxo = await determine(true) + if (mainUtxo) { + return mainUtxo + } + + // In case the main UTXO was not found for witness address, there is still + // a chance it exists for the legacy wallet address. + return determine(false) +} diff --git a/typescript/test/wallet.test.ts b/typescript/test/wallet.test.ts new file mode 100644 index 000000000..8042be548 --- /dev/null +++ b/typescript/test/wallet.test.ts @@ -0,0 +1,246 @@ +import { MockBitcoinClient } from "./utils/mock-bitcoin-client" +import { MockBridge } from "./utils/mock-bridge" +import { BitcoinNetwork, BitcoinTransaction, Hex } from "../src" +import { determineWalletMainUtxo, Wallet } from "../src/wallet" +import { expect } from "chai" +import { encodeToBitcoinAddress } from "../src/bitcoin" +import { BigNumber } from "ethers" + +describe("Wallet", () => { + describe("determineWalletMainUtxo", () => { + // Just an arbitrary 20-byte wallet public key hash. + const walletPublicKeyHash = Hex.from( + "e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0" + ) + + // Helper function facilitating creation of mock transactions. + const mockTransaction = ( + hash: string, + outputs: Record // key: locking script, value: amount of locked satoshis + ): BitcoinTransaction => { + return { + transactionHash: Hex.from(hash), + inputs: [], // not relevant in this test scenario + outputs: Object.entries(outputs).map( + ([scriptPubKey, value], index) => ({ + outputIndex: index, + value: BigNumber.from(value), + scriptPubKey: Hex.from(scriptPubKey), + }) + ), + } + } + + // Create a fake wallet witness transaction history that consists of 6 transactions. + const walletWitnessTransactionHistory: BitcoinTransaction[] = [ + mockTransaction( + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1", + { + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6", + { + "00140000000000000000000000000000000000000001": 100000, + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 200000, // wallet witness output + } + ), + mockTransaction( + "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6", + { + "00140000000000000000000000000000000000000001": 100000, + "00140000000000000000000000000000000000000002": 200000, + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 300000, // wallet witness output + } + ), + mockTransaction( + "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e", + { + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b", + { + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214", + { + "00140000000000000000000000000000000000000001": 100000, + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 200000, // wallet witness output + } + ), + ] + + // Create a fake wallet legacy transaction history that consists of 2 transactions. + const walletLegacyTransactionsHistory: BitcoinTransaction[] = [ + mockTransaction( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94", + { + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "00cc0cd13fc4de7a15cb41ab6d58f8b31c75b6b9b4194958c381441a67d09b08", + { + "00140000000000000000000000000000000000000001": 100000, + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 200000, // wallet legacy output + } + ), + ] + + let bridge: MockBridge + let bitcoinClient: MockBitcoinClient + let bitcoinNetwork: BitcoinNetwork + + beforeEach(async () => { + bridge = new MockBridge() + bitcoinClient = new MockBitcoinClient() + }) + + context("when wallet main UTXO is not set in the Bridge", () => { + beforeEach(async () => { + bridge.setWallet(walletPublicKeyHash.toPrefixedString(), { + mainUtxoHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + } as Wallet) + }) + + it("should return undefined", async () => { + const mainUtxo = await determineWalletMainUtxo( + walletPublicKeyHash, + bridge, + bitcoinClient, + bitcoinNetwork + ) + + expect(mainUtxo).to.be.undefined + }) + }) + + context("when wallet main UTXO is set in the Bridge", () => { + const tests = [ + { + testName: "recent witness transaction", + // Set the main UTXO hash based on the latest transaction from walletWitnessTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + expectedMainUtxo: { + transactionHash: Hex.from( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + }, + { + testName: "recent legacy transaction", + // Set the main UTXO hash based on the second last transaction from walletLegacyTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + expectedMainUtxo: { + transactionHash: Hex.from( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + }, + { + testName: "old witness transaction", + // Set the main UTXO hash based on the oldest transaction from walletWitnessTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + expectedMainUtxo: undefined, + }, + ] + + tests.forEach(({ testName, actualMainUtxo, expectedMainUtxo }) => { + context(`with main UTXO coming from ${testName}`, () => { + const networkTests = [ + { + networkTestName: "bitcoin testnet", + network: BitcoinNetwork.Testnet, + }, + { + networkTestName: "bitcoin mainnet", + network: BitcoinNetwork.Mainnet, + }, + ] + + networkTests.forEach(({ networkTestName, network }) => { + context(`with ${networkTestName} network`, () => { + beforeEach(async () => { + bitcoinNetwork = network + + const walletWitnessAddress = encodeToBitcoinAddress( + walletPublicKeyHash.toString(), + true, + bitcoinNetwork + ) + const walletLegacyAddress = encodeToBitcoinAddress( + walletPublicKeyHash.toString(), + false, + bitcoinNetwork + ) + + // Record the fake transaction history for both address types. + const transactionHistory = new Map< + string, + BitcoinTransaction[] + >() + transactionHistory.set( + walletWitnessAddress, + walletWitnessTransactionHistory + ) + transactionHistory.set( + walletLegacyAddress, + walletLegacyTransactionsHistory + ) + bitcoinClient.transactionHistory = transactionHistory + + bridge.setWallet(walletPublicKeyHash.toPrefixedString(), { + mainUtxoHash: bridge.buildUtxoHash(actualMainUtxo), + } as Wallet) + }) + + it("should return the expected main UTXO", async () => { + const mainUtxo = await determineWalletMainUtxo( + walletPublicKeyHash, + bridge, + bitcoinClient, + bitcoinNetwork + ) + + expect(mainUtxo).to.be.eql(expectedMainUtxo) + }) + }) + }) + }) + }) + }) + }) +}) From 10ab6dabb5a1e400607ed2b27411a24080569071 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 11 Jul 2023 18:11:53 +0200 Subject: [PATCH 26/30] Make `findWalletForRedemption` use `determineWalletMainUtxo` function The `findWalletForRedemption` function should rely on `determineWalletMainUtxo` wallet while seeking for current wallet main UTXO. Otherwise, it may not find the main UTXO in case there is a wallet state drift between Bitcoin and Ethereum chains. --- typescript/src/redemption.ts | 57 +++++------------- typescript/test/data/redemption.ts | 92 ++++++++++++++++++++---------- typescript/test/redemption.test.ts | 50 ++++------------ 3 files changed, 85 insertions(+), 114 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 05a2a6dda..f8a0665cf 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -7,11 +7,10 @@ import { UnspentTransactionOutput, Client as BitcoinClient, TransactionHash, - encodeToBitcoinAddress, } from "./bitcoin" import { Bridge, Identifier, TBTCToken } from "./chain" import { assembleTransactionProof } from "./proof" -import { WalletState } from "./wallet" +import { determineWalletMainUtxo, WalletState } from "./wallet" import { BitcoinNetwork } from "./bitcoin-network" import { Hex } from "./hex" @@ -445,7 +444,7 @@ export async function findWalletForRedemption( for (const wallet of wallets) { const { walletPublicKeyHash } = wallet - const { state, mainUtxoHash, walletPublicKey, pendingRedemptionsValue } = + const { state, walletPublicKey, pendingRedemptionsValue } = await bridge.wallets(walletPublicKeyHash) // Wallet must be in Live state. @@ -458,18 +457,20 @@ export async function findWalletForRedemption( continue } - if ( - mainUtxoHash.equals( - Hex.from( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ) - ) - ) { + // Wallet must have a main UTXO that can be determined. + const mainUtxo = await determineWalletMainUtxo( + walletPublicKeyHash, + bridge, + bitcoinClient, + bitcoinNetwork + ) + if (!mainUtxo) { console.debug( - `Main utxo not set for wallet public ` + - `key hash(${walletPublicKeyHash.toString()}). ` + + `Could not find matching UTXO on chains ` + + `for wallet public key hash (${walletPublicKeyHash.toString()}). ` + `Continue the loop execution to the next wallet...` ) + continue } const pendingRedemption = await bridge.pendingRedemptions( @@ -489,38 +490,6 @@ export async function findWalletForRedemption( continue } - const walletBitcoinAddress = encodeToBitcoinAddress( - wallet.walletPublicKeyHash.toString(), - true, - 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 - ) - - // 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 mainUtxo = utxos.find((utxo) => - mainUtxoHash.equals(bridge.buildUtxoHash(utxo)) - ) - - if (!mainUtxo) { - console.debug( - `Could not find matching UTXO on chains ` + - `for wallet public key hash(${walletPublicKeyHash.toString()}). ` + - `Continue the loop execution to the next wallet...` - ) - continue - } - const walletBTCBalance = mainUtxo.value.sub(pendingRedemptionsValue) // Save the max possible redemption amount. diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 285169c11..958b9dcc2 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -7,10 +7,11 @@ import { UnspentTransactionOutput, TransactionMerkleBranch, TransactionHash, + createOutputScriptFromAddress, } from "../../src/bitcoin" import { RedemptionRequest } from "../../src/redemption" import { Address } from "../../src/ethereum" -import { Hex } from "../../src" +import { BitcoinTransaction, Hex } from "../../src" import { WalletState } from "../../src/wallet" /** @@ -668,13 +669,14 @@ export const redemptionProof: RedemptionProofTestData = { }, } -interface FindWalletForRedemptionWaleltData { +interface FindWalletForRedemptionWalletData { data: { state: WalletState mainUtxoHash: Hex walletPublicKey: Hex btcAddress: string - utxos: UnspentTransactionOutput[] + mainUtxo: UnspentTransactionOutput + transactions: BitcoinTransaction[] pendingRedemptionsValue: BigNumber } event: { @@ -687,10 +689,10 @@ interface FindWalletForRedemptionWaleltData { } export const findWalletForRedemptionData: { - liveWallet: FindWalletForRedemptionWaleltData - walletWithoutUtxo: FindWalletForRedemptionWaleltData - nonLiveWallet: FindWalletForRedemptionWaleltData - walletWithPendingRedemption: FindWalletForRedemptionWaleltData + liveWallet: FindWalletForRedemptionWalletData + walletWithoutUtxo: FindWalletForRedemptionWalletData + nonLiveWallet: FindWalletForRedemptionWalletData + walletWithPendingRedemption: FindWalletForRedemptionWalletData pendingRedemption: RedemptionRequest } = { liveWallet: { @@ -703,13 +705,28 @@ export const findWalletForRedemptionData: { "0x028ed84936be6a9f594a2dcc636d4bebf132713da3ce4dac5c61afbf8bbb47d6f7" ), btcAddress: "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja", - utxos: [ + mainUtxo: { + transactionHash: Hex.from( + "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" + ), + outputIndex: 0, + value: BigNumber.from("791613461"), + }, + transactions: [ { transactionHash: Hex.from( "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" ), - outputIndex: 0, - value: BigNumber.from("791613461"), + inputs: [], // not relevant + outputs: [ + { + outputIndex: 0, + value: BigNumber.from("791613461"), + scriptPubKey: createOutputScriptFromAddress( + "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja" + ), + }, + ], }, ], pendingRedemptionsValue: BigNumber.from(0), @@ -740,15 +757,14 @@ export const findWalletForRedemptionData: { "0x030fbbae74e6d85342819e719575949a1349e975b69fb382e9fef671a3a74efc52" ), btcAddress: "tb1qkct7r24k4wutnsun84rvp3qsyt8yfpvqz89d2y", - utxos: [ - { - transactionHash: Hex.from( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ), - outputIndex: 0, - value: BigNumber.from("0"), - }, - ], + mainUtxo: { + transactionHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + outputIndex: 0, + value: BigNumber.from("0"), + }, + transactions: [], pendingRedemptionsValue: BigNumber.from(0), }, event: { @@ -778,15 +794,14 @@ export const findWalletForRedemptionData: { "0x02633b102417009ae55103798f4d366dfccb081dcf20025088b9bf10a8e15d8ded" ), btcAddress: "tb1qf6jvyd680ncf9dtr5znha9ql5jmw84lupwwuf6", - utxos: [ - { - transactionHash: Hex.from( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ), - outputIndex: 0, - value: BigNumber.from("0"), - }, - ], + mainUtxo: { + transactionHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + outputIndex: 0, + value: BigNumber.from("0"), + }, + transactions: [], pendingRedemptionsValue: BigNumber.from(0), }, event: { @@ -815,13 +830,28 @@ export const findWalletForRedemptionData: { "0x02ab193a63b3523bfab77d3645d11da10722393687458c4213b350b7e08f50b7ee" ), btcAddress: "tb1qx2xejtjltdcau5dpks8ucszkhxdg3fj88404lh", - utxos: [ + mainUtxo: { + transactionHash: Hex.from( + "0x81c4884a8c2fccbeb57745a5e59f895a9c1bb8fc42eecc82269100a1a46bbb85" + ), + outputIndex: 0, + value: BigNumber.from("3370000"), // 0.0337 BTC + }, + transactions: [ { transactionHash: Hex.from( "0x81c4884a8c2fccbeb57745a5e59f895a9c1bb8fc42eecc82269100a1a46bbb85" ), - outputIndex: 0, - value: BigNumber.from("3370000"), // 0.0337 BTC + inputs: [], // not relevant + outputs: [ + { + outputIndex: 0, + value: BigNumber.from("3370000"), // 0.0337 BTC + scriptPubKey: createOutputScriptFromAddress( + "tb1qx2xejtjltdcau5dpks8ucszkhxdg3fj88404lh" + ), + }, + ], }, ], pendingRedemptionsValue: BigNumber.from(2370000), // 0.0237 BTC diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 15b321075..e2efa36d1 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -39,6 +39,7 @@ import { BigNumberish, BigNumber } from "ethers" import { BitcoinNetwork } from "../src/bitcoin-network" import { Wallet } from "../src/wallet" import { MockTBTCToken } from "./utils/mock-tbtc-token" +import { BitcoinTransaction } from "../src" chai.use(chaiAsPromised) @@ -1490,9 +1491,9 @@ describe("Redemption", () => { (wallet) => wallet.event ) - const walletsUnspentTransacionOutputs = new Map< + const walletsTransactionHistory = new Map< string, - UnspentTransactionOutput[] + BitcoinTransaction[] >() walletsOrder.forEach((wallet) => { @@ -1501,11 +1502,11 @@ describe("Redemption", () => { mainUtxoHash, walletPublicKey, btcAddress, - utxos, + transactions, pendingRedemptionsValue, } = wallet.data - walletsUnspentTransacionOutputs.set(btcAddress, utxos) + walletsTransactionHistory.set(btcAddress, transactions) bridge.setWallet( wallet.event.walletPublicKeyHash.toPrefixedString(), { @@ -1517,8 +1518,7 @@ describe("Redemption", () => { ) }) - bitcoinClient.unspentTransactionOutputs = - walletsUnspentTransacionOutputs + bitcoinClient.transactionHistory = walletsTransactionHistory }) context( @@ -1545,29 +1545,13 @@ describe("Redemption", () => { }) }) - it("should get wallet data details", () => { - const bridgeWalletDetailsLogs = bridge.walletsLog - - 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 expectedWalletData = findWalletForRedemptionData.walletWithPendingRedemption.data expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUtxo: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.mainUtxo, }) }) } @@ -1579,8 +1563,7 @@ describe("Redemption", () => { const amount = BigNumber.from("10000000000") // 1 000 BTC const expectedMaxAmount = walletsOrder .map((wallet) => wallet.data) - .map((wallet) => wallet.utxos) - .flat() + .map((wallet) => wallet.mainUtxo) .map((utxo) => utxo.value) .sort((a, b) => (b.gt(a) ? 0 : -1))[0] @@ -1647,24 +1630,13 @@ describe("Redemption", () => { }) }) - it("should get wallet data details", () => { - const bridgeWalletDetailsLogs = bridge.walletsLog - - 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 expectedWalletData = findWalletForRedemptionData.liveWallet.data expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUtxo: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.mainUtxo, }) }) } @@ -1676,7 +1648,7 @@ describe("Redemption", () => { beforeEach(async () => { const wallet = findWalletForRedemptionData.walletWithPendingRedemption - const walletBTCBalance = wallet.data.utxos[0].value + const walletBTCBalance = wallet.data.mainUtxo.value const amount: BigNumber = walletBTCBalance .sub(wallet.data.pendingRedemptionsValue) @@ -1699,7 +1671,7 @@ describe("Redemption", () => { expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUtxo: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.mainUtxo, }) }) } From 0e369b2f3ba1b80e23b324c5a59bcd5bf88c699b Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:08:28 +0200 Subject: [PATCH 27/30] Add `console.error` if wallet output not found for wallet transaction --- typescript/src/wallet.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typescript/src/wallet.ts b/typescript/src/wallet.ts index 2d1bccbf3..7f053b79a 100644 --- a/typescript/src/wallet.ts +++ b/typescript/src/wallet.ts @@ -299,6 +299,9 @@ export async function determineWalletMainUtxo( // Should never happen as all transactions come from wallet history. Just // in case check whether the wallet output was actually found. if (outputIndex < 0) { + console.error( + `wallet output for transaction ${walletTransaction.transactionHash.toString()} not found` + ) continue } From 056cac5b657d984d3ddba4605b879a4e540780fe Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:18:00 +0200 Subject: [PATCH 28/30] Use `createOutputScriptFromAddress` where applicable --- typescript/src/electrum.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typescript/src/electrum.ts b/typescript/src/electrum.ts index d467f42f7..022ccce2a 100644 --- a/typescript/src/electrum.ts +++ b/typescript/src/electrum.ts @@ -2,6 +2,7 @@ import bcoin from "bcoin" import pTimeout from "p-timeout" import { Client as BitcoinClient, + createOutputScriptFromAddress, RawTransaction, Transaction, TransactionHash, @@ -232,7 +233,7 @@ export class Client implements BitcoinClient { ): Promise { return this.withElectrum( async (electrum: Electrum) => { - const script = bcoin.Script.fromAddress(address).toRaw().toString("hex") + const script = createOutputScriptFromAddress(address).toString() // eslint-disable-next-line camelcase type UnspentOutput = { tx_pos: number; value: number; tx_hash: string } @@ -262,7 +263,7 @@ export class Client implements BitcoinClient { limit?: number ): Promise { return this.withElectrum(async (electrum: Electrum) => { - const script = bcoin.Script.fromAddress(address).toRaw().toString("hex") + const script = createOutputScriptFromAddress(address).toString() // eslint-disable-next-line camelcase type HistoryItem = { height: number; tx_hash: string } From 7b0b9bdb7fb947614f9ddff195c73f3a001bb7b9 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:20:42 +0200 Subject: [PATCH 29/30] Simplify `determineWalletMainUtxo` return statement --- typescript/src/wallet.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/typescript/src/wallet.ts b/typescript/src/wallet.ts index 7f053b79a..5c9a46ae1 100644 --- a/typescript/src/wallet.ts +++ b/typescript/src/wallet.ts @@ -326,11 +326,8 @@ export async function determineWalletMainUtxo( // operations. Try to determine the main UTXO for that address first as the // chance for success is the highest here. const mainUtxo = await determine(true) - if (mainUtxo) { - return mainUtxo - } // In case the main UTXO was not found for witness address, there is still // a chance it exists for the legacy wallet address. - return determine(false) + return mainUtxo ?? (await determine(false)) } From f2e5b9f938a6e455609a7c5bb9933bd5fa046492 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:27:48 +0200 Subject: [PATCH 30/30] Improve unit tests of `determineWalletMainUtxo` --- typescript/test/wallet.test.ts | 47 +++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/typescript/test/wallet.test.ts b/typescript/test/wallet.test.ts index 8042be548..277b07e5a 100644 --- a/typescript/test/wallet.test.ts +++ b/typescript/test/wallet.test.ts @@ -78,8 +78,37 @@ describe("Wallet", () => { ), ] - // Create a fake wallet legacy transaction history that consists of 2 transactions. - const walletLegacyTransactionsHistory: BitcoinTransaction[] = [ + // Create a fake wallet legacy transaction history that consists of 6 transactions. + const walletLegacyTransactionHistory: BitcoinTransaction[] = [ + mockTransaction( + "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267", + { + "00140000000000000000000000000000000000000001": 100000, + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 200000, // wallet legacy output + } + ), + mockTransaction( + "b11bfc481b95769b8488bd661d5f61a35f7c3c757160494d63f6e04e532dfcb9", + { + "00140000000000000000000000000000000000000001": 100000, + "00140000000000000000000000000000000000000002": 200000, + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 300000, // wallet legacy output + } + ), + mockTransaction( + "7e91580d989f8541489a37431381ff9babd596111232f1bc7a1a1ba503c27dee", + { + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "5404e339ba82e6e52fcc24cb40029bed8425baa4c7f869626ef9de956186f910", + { + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output + "00140000000000000000000000000000000000000001": 200000, + } + ), mockTransaction( "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94", { @@ -176,6 +205,18 @@ describe("Wallet", () => { }, expectedMainUtxo: undefined, }, + { + testName: "old legacy transaction", + // Set the main UTXO hash based on the oldest transaction from walletLegacyTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + expectedMainUtxo: undefined, + }, ] tests.forEach(({ testName, actualMainUtxo, expectedMainUtxo }) => { @@ -218,7 +259,7 @@ describe("Wallet", () => { ) transactionHistory.set( walletLegacyAddress, - walletLegacyTransactionsHistory + walletLegacyTransactionHistory ) bitcoinClient.transactionHistory = transactionHistory