diff --git a/lib/Boltz.ts b/lib/Boltz.ts index f727654c..5e782f62 100644 --- a/lib/Boltz.ts +++ b/lib/Boltz.ts @@ -23,6 +23,7 @@ import LndClient from './lightning/LndClient'; import ClnClient from './lightning/cln/ClnClient'; import MpayClient from './lightning/cln/MpayClient'; import NotificationProvider from './notifications/NotificationProvider'; +import Blocks from './service/Blocks'; import CountryCodes from './service/CountryCodes'; import Service from './service/Service'; import NodeSwitch from './swap/NodeSwitch'; @@ -44,6 +45,7 @@ class Boltz { private readonly notifications!: NotificationProvider; private readonly api!: Api; + private readonly blocks: Blocks; private readonly countryCodes: CountryCodes; private readonly grpcServer!: GrpcServer; private readonly prometheus: Prometheus; @@ -115,6 +117,8 @@ class Boltz { this.ethereumManagers, ); + this.blocks = new Blocks(this.logger, this.config.blocks); + try { this.service = new Service( this.logger, @@ -122,6 +126,7 @@ class Boltz { this.walletManager, new NodeSwitch(this.logger, this.config.nodeSwitch), this.currencies, + this.blocks, ); this.backup = new BackupScheduler( @@ -214,7 +219,10 @@ class Boltz { await this.grpcServer.listen(); - await this.countryCodes.downloadRanges(); + await Promise.all([ + this.countryCodes.downloadRanges(), + this.blocks.updateBlocks(), + ]); await this.api.init(); // Rescan chains after everything else was initialized to avoid race conditions diff --git a/lib/Config.ts b/lib/Config.ts index 05c026f7..202e6246 100644 --- a/lib/Config.ts +++ b/lib/Config.ts @@ -10,7 +10,8 @@ import { Network } from './consts/Enums'; import Errors from './consts/Errors'; import { PairConfig } from './consts/Types'; import { LndConfig } from './lightning/LndClient'; -import { ClnConfig } from './lightning/cln/ClnClient'; +import { ClnConfig } from './lightning/cln/Types'; +import { BlocksConfig } from './service/Blocks'; import { MarkingsConfig } from './service/CountryCodes'; import { NodeSwitchConfig } from './swap/NodeSwitch'; @@ -164,6 +165,7 @@ type ConfigType = { swapwitnessaddress: boolean; marking: MarkingsConfig; + blocks: BlocksConfig; api: ApiConfig; grpc: GrpcConfig; @@ -228,6 +230,8 @@ class Config { 'https://cdn.jsdelivr.net/npm/@ip-location-db/asn-country/asn-country-ipv6-num.csv', }, + blocks: {}, + api: { host: '127.0.0.1', port: 9001, diff --git a/lib/cli/Command.ts b/lib/cli/Command.ts index 6618362b..228f3e7c 100644 --- a/lib/cli/Command.ts +++ b/lib/cli/Command.ts @@ -8,6 +8,7 @@ import { detectSwap, } from 'boltz-core'; import { Networks as LiquidNetworks } from 'boltz-core/dist/lib/liquid'; +import * as console from 'console'; import { randomBytes } from 'crypto'; import { ECPairInterface } from 'ecpair'; import { Transaction as LiquidTransaction } from 'liquidjs-lib'; @@ -87,7 +88,9 @@ export const prepareTx = async ( // If the redeem script can be parsed as JSON, it is a swap tree try { - const tree = SwapTreeSerializer.deserializeSwapTree(argv.redeemScript); + const tree = SwapTreeSerializer.deserializeSwapTree( + argv.redeemScript.replaceAll('\\"', '"'), + ); const { musig, swapOutput } = musigFromExtractedKey( type, diff --git a/lib/service/Blocks.ts b/lib/service/Blocks.ts new file mode 100644 index 00000000..354f34ee --- /dev/null +++ b/lib/service/Blocks.ts @@ -0,0 +1,40 @@ +import axios from 'axios'; +import Logger from '../Logger'; + +type BlocksConfig = { + urls?: string[]; +}; + +class Blocks { + private blocked = new Set(); + + constructor( + private readonly logger: Logger, + private readonly config: BlocksConfig, + ) {} + + public updateBlocks = async () => { + if (this.config.urls === undefined || this.config.urls.length === 0) { + return; + } + + const addresses = await Promise.all( + this.config.urls.map(async (url) => { + const response = await axios.get(url); + this.logger.verbose( + `Fetched ${response.data.length} blocked addresses from: ${url}`, + ); + return response.data; + }), + ); + + for (const addr of addresses.flat()) { + this.blocked.add(addr); + } + }; + + public isBlocked = (addr: string) => this.blocked.has(addr); +} + +export default Blocks; +export { BlocksConfig }; diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 30dbe79d..87db3e06 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -64,6 +64,7 @@ import SwapManager, { ChannelCreationInfo } from '../swap/SwapManager'; import SwapOutputType from '../swap/SwapOutputType'; import WalletManager, { Currency } from '../wallet/WalletManager'; import EthereumManager from '../wallet/ethereum/EthereumManager'; +import Blocks from './Blocks'; import ElementsService from './ElementsService'; import Errors from './Errors'; import EventHandler from './EventHandler'; @@ -119,6 +120,7 @@ class Service { private walletManager: WalletManager, private nodeSwitch: NodeSwitch, public currencies: Map, + blocks: Blocks, ) { this.prepayMinerFee = config.prepayminerfee; this.logger.debug( @@ -163,6 +165,7 @@ class Service { : OutputType.Compatibility, ), config.retryInterval, + blocks, ); this.eventHandler = new EventHandler( diff --git a/lib/swap/Errors.ts b/lib/swap/Errors.ts index a3bacfa6..951a2673 100644 --- a/lib/swap/Errors.ts +++ b/lib/swap/Errors.ts @@ -98,4 +98,8 @@ export default { message: 'invalid address', code: concatErrorCode(ErrorCodePrefix.Swap, 19), }), + BLOCKED_ADDRESS: (): Error => ({ + message: 'blocked address', + code: concatErrorCode(ErrorCodePrefix.Swap, 20), + }), }; diff --git a/lib/swap/EthereumNursery.ts b/lib/swap/EthereumNursery.ts index aedfd76e..53239aea 100644 --- a/lib/swap/EthereumNursery.ts +++ b/lib/swap/EthereumNursery.ts @@ -1,4 +1,4 @@ -import { TransactionResponse } from 'ethers'; +import { Transaction, TransactionResponse } from 'ethers'; import { EventEmitter } from 'events'; import { Op } from 'sequelize'; import Logger from '../Logger'; @@ -15,6 +15,7 @@ import ReverseSwap from '../db/models/ReverseSwap'; import Swap from '../db/models/Swap'; import ReverseSwapRepository from '../db/repositories/ReverseSwapRepository'; import SwapRepository from '../db/repositories/SwapRepository'; +import Blocks from '../service/Blocks'; import Wallet from '../wallet/Wallet'; import WalletManager from '../wallet/WalletManager'; import EthereumManager from '../wallet/ethereum/EthereumManager'; @@ -109,6 +110,7 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { private readonly logger: Logger, private readonly walletManager: WalletManager, public readonly ethereumManager: EthereumManager, + private readonly blocks: Blocks, ) { super(); @@ -184,7 +186,7 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { private listenEtherSwap = () => { this.ethereumManager.contractEventHandler.on( 'eth.lockup', - async (transactionHash, etherSwapValues) => { + async (transaction: Transaction, etherSwapValues) => { let swap = await SwapRepository.getSwap({ preimageHash: getHexString(etherSwapValues.preimageHash), status: { @@ -209,12 +211,12 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { } this.logger.debug( - `Found lockup in ${this.ethereumManager.networkDetails.name} EtherSwap contract for Swap ${swap.id}: ${transactionHash}`, + `Found lockup in ${this.ethereumManager.networkDetails.name} EtherSwap contract for Swap ${swap.id}: ${transaction.hash}`, ); swap = await SwapRepository.setLockupTransaction( swap, - transactionHash, + transaction.hash!, Number(etherSwapValues.amount / etherDecimals), true, ); @@ -261,7 +263,12 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { } } - this.emit('eth.lockup', swap, transactionHash, etherSwapValues); + if (this.blocks.isBlocked(transaction.from!)) { + this.emit('lockup.failed', swap, Errors.BLOCKED_ADDRESS().message); + return; + } + + this.emit('eth.lockup', swap, transaction.hash, etherSwapValues); }, ); @@ -291,7 +298,7 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { private listenERC20Swap = () => { this.ethereumManager.contractEventHandler.on( 'erc20.lockup', - async (transactionHash, erc20SwapValues) => { + async (transaction: Transaction, erc20SwapValues) => { let swap = await SwapRepository.getSwap({ preimageHash: getHexString(erc20SwapValues.preimageHash), status: { @@ -320,12 +327,12 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { const erc20Wallet = wallet.walletProvider as ERC20WalletProvider; this.logger.debug( - `Found lockup in ${this.ethereumManager.networkDetails.name} ERC20Swap contract for Swap ${swap.id}: ${transactionHash}`, + `Found lockup in ${this.ethereumManager.networkDetails.name} ERC20Swap contract for Swap ${swap.id}: ${transaction.hash}`, ); swap = await SwapRepository.setLockupTransaction( swap, - transactionHash, + transaction.hash!, erc20Wallet.normalizeTokenAmount(erc20SwapValues.amount), true, ); @@ -383,7 +390,12 @@ class EthereumNursery extends EventEmitter implements IEthereumNursery { } } - this.emit('erc20.lockup', swap, transactionHash, erc20SwapValues); + if (this.blocks.isBlocked(transaction.from!)) { + this.emit('lockup.failed', swap, Errors.BLOCKED_ADDRESS().message); + return; + } + + this.emit('erc20.lockup', swap, transaction.hash, erc20SwapValues); }, ); diff --git a/lib/swap/SwapManager.ts b/lib/swap/SwapManager.ts index 8aa934d9..72bc02b8 100644 --- a/lib/swap/SwapManager.ts +++ b/lib/swap/SwapManager.ts @@ -45,6 +45,7 @@ import ChannelCreationRepository from '../db/repositories/ChannelCreationReposit import ReverseSwapRepository from '../db/repositories/ReverseSwapRepository'; import SwapRepository from '../db/repositories/SwapRepository'; import RateProvider from '../rates/RateProvider'; +import Blocks from '../service/Blocks'; import InvoiceExpiryHelper from '../service/InvoiceExpiryHelper'; import PaymentRequestUtils from '../service/PaymentRequestUtils'; import TimeoutDeltaProvider from '../service/TimeoutDeltaProvider'; @@ -133,6 +134,7 @@ class SwapManager { private readonly paymentRequestUtils: PaymentRequestUtils, private readonly swapOutputType: SwapOutputType, retryInterval: number, + private readonly blocks: Blocks, ) { this.nursery = new SwapNursery( this.logger, @@ -142,6 +144,7 @@ class SwapManager { this.walletManager, this.swapOutputType, retryInterval, + this.blocks, ); } @@ -593,6 +596,14 @@ class SwapManager { ); } + const isBitcoinLike = + sendingCurrency.type === CurrencyType.BitcoinLike || + sendingCurrency.type === CurrencyType.Liquid; + + if (!isBitcoinLike && this.blocks.isBlocked(args.claimAddress!)) { + throw Errors.BLOCKED_ADDRESS(); + } + const pair = getPairId({ base: args.baseCurrency, quote: args.quoteCurrency, @@ -672,10 +683,7 @@ class SwapManager { invoice: paymentRequest, }; - if ( - sendingCurrency.type === CurrencyType.BitcoinLike || - sendingCurrency.type === CurrencyType.Liquid - ) { + if (isBitcoinLike) { const { keys, index } = sendingCurrency.wallet.getNewKeys(); const { blocks } = await sendingCurrency.chainClient!.getBlockchainInfo(); result.timeoutBlockHeight = blocks + args.onchainTimeoutBlockDelta; diff --git a/lib/swap/SwapNursery.ts b/lib/swap/SwapNursery.ts index 6ef64ff7..fc8040d7 100644 --- a/lib/swap/SwapNursery.ts +++ b/lib/swap/SwapNursery.ts @@ -45,6 +45,7 @@ import { } from '../lightning/LightningClient'; import FeeProvider from '../rates/FeeProvider'; import RateProvider from '../rates/RateProvider'; +import Blocks from '../service/Blocks'; import TimeoutDeltaProvider from '../service/TimeoutDeltaProvider'; import Wallet from '../wallet/Wallet'; import WalletManager, { Currency } from '../wallet/WalletManager'; @@ -198,12 +199,13 @@ class SwapNursery extends EventEmitter implements ISwapNursery { private walletManager: WalletManager, private swapOutputType: SwapOutputType, private retryInterval: number, + blocks: Blocks, ) { super(); this.logger.info(`Setting Swap retry interval to ${retryInterval} seconds`); - this.utxoNursery = new UtxoNursery(this.logger, this.walletManager); + this.utxoNursery = new UtxoNursery(this.logger, this.walletManager, blocks); this.lightningNursery = new LightningNursery(this.logger); this.invoiceNursery = new InvoiceNursery(this.logger); this.channelNursery = new ChannelNursery( @@ -213,7 +215,7 @@ class SwapNursery extends EventEmitter implements ISwapNursery { this.ethereumNurseries = this.walletManager.ethereumManagers.map( (manager) => - new EthereumNursery(this.logger, this.walletManager, manager), + new EthereumNursery(this.logger, this.walletManager, manager, blocks), ); this.paymentHandler = new PaymentHandler( diff --git a/lib/swap/UtxoNursery.ts b/lib/swap/UtxoNursery.ts index b3ce3263..982b9529 100644 --- a/lib/swap/UtxoNursery.ts +++ b/lib/swap/UtxoNursery.ts @@ -15,18 +15,21 @@ import Logger from '../Logger'; import { getChainCurrency, getHexBuffer, + getHexString, reverseBuffer, splitPairId, transactionHashToId, transactionSignalsRbfExplicitly, } from '../Utils'; import ChainClient from '../chain/ChainClient'; -import { SwapUpdateEvent, SwapVersion } from '../consts/Enums'; +import { CurrencyType, SwapUpdateEvent, SwapVersion } from '../consts/Enums'; import ReverseSwap from '../db/models/ReverseSwap'; import Swap from '../db/models/Swap'; import ReverseSwapRepository from '../db/repositories/ReverseSwapRepository'; import SwapRepository from '../db/repositories/SwapRepository'; +import Blocks from '../service/Blocks'; import Wallet from '../wallet/Wallet'; +import WalletLiquid from '../wallet/WalletLiquid'; import WalletManager, { Currency } from '../wallet/WalletManager'; import Errors from './Errors'; @@ -111,8 +114,9 @@ class UtxoNursery extends EventEmitter { 'reverseSwapLockupConfirmation'; constructor( - private logger: Logger, - private walletManager: WalletManager, + private readonly logger: Logger, + private readonly walletManager: WalletManager, + private readonly blocks: Blocks, ) { super(); } @@ -471,7 +475,7 @@ class UtxoNursery extends EventEmitter { const swapOutput = detectSwap(redeemScriptOrTweakedKey, transaction)!; this.logger.verbose( - `Found ${confirmed ? '' : 'un'}confirmed lockup transaction for Swap ${ + `Found ${confirmed ? '' : 'un'}confirmed ${wallet.symbol} lockup transaction for Swap ${ swap.id }: ${transaction.getId()}:${swapOutput.vout}`, ); @@ -499,6 +503,36 @@ class UtxoNursery extends EventEmitter { } } + const inputTxs = ( + await Promise.all( + Array.from( + new Set( + transaction.ins.map((input) => + getHexString(reverseBuffer(input.hash)), + ), + ).values(), + ).map((id) => chainClient.getRawTransaction(id)), + ) + ).map((txHex) => parseTransaction(wallet.type, txHex)); + + const prevAddreses = inputTxs + .map((tx) => tx.outs) + .flat() + .map((output) => + wallet.type === CurrencyType.Liquid + ? (wallet as WalletLiquid).encodeAddress(output.script, false) + : wallet.encodeAddress(output.script), + ); + + if (prevAddreses.some(this.blocks.isBlocked)) { + this.emit( + 'swap.lockup.failed', + updatedSwap, + Errors.BLOCKED_ADDRESS().message, + ); + return; + } + // Confirmed transactions do not have to be checked for 0-conf criteria if (!confirmed) { if (updatedSwap.acceptZeroConf !== true) { diff --git a/lib/wallet/ethereum/ContractEventHandler.ts b/lib/wallet/ethereum/ContractEventHandler.ts index 701fa401..0eea8065 100644 --- a/lib/wallet/ethereum/ContractEventHandler.ts +++ b/lib/wallet/ethereum/ContractEventHandler.ts @@ -1,6 +1,6 @@ import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; -import { ContractEventPayload } from 'ethers'; +import { ContractEventPayload, Transaction } from 'ethers'; import { EventEmitter } from 'events'; import Logger from '../../Logger'; import { ERC20SwapValues, EtherSwapValues } from '../../consts/Types'; @@ -13,13 +13,13 @@ interface IContractEventHandler { on( event: 'eth.lockup', listener: ( - transactionHash: string, + transaction: Transaction, etherSwapValues: EtherSwapValues, ) => void, ): this; emit( event: 'eth.lockup', - transactionHash: string, + transaction: Transaction, etherSwapValues: EtherSwapValues, ): boolean; @@ -52,13 +52,14 @@ interface IContractEventHandler { on( event: 'erc20.lockup', listener: ( - transactionHash: string, + transaction: Transaction, + erc20SwapValues: ERC20SwapValues, ) => void, ): this; emit( event: 'erc20.lockup', - transactionHash: string, + transaction: Transaction, erc20SwapValues: ERC20SwapValues, ): boolean; @@ -95,7 +96,7 @@ class ContractEventHandler private etherSwap!: EtherSwap; private erc20Swap!: ERC20Swap; - constructor(private logger: Logger) { + constructor(private readonly logger: Logger) { super(); } @@ -115,85 +116,67 @@ class ContractEventHandler }; public rescan = async (startHeight: number): Promise => { - const etherLockups = await this.etherSwap.queryFilter( - this.etherSwap.filters.Lockup(), - startHeight, - ); - - const etherClaims = await this.etherSwap.queryFilter( - this.etherSwap.filters.Claim(), - startHeight, - ); - - const etherRefunds = await this.etherSwap.queryFilter( - this.etherSwap.filters.Refund(), - startHeight, - ); + const [etherLockups, etherClaims, etherRefunds] = await Promise.all([ + this.etherSwap.queryFilter(this.etherSwap.filters.Lockup(), startHeight), + this.etherSwap.queryFilter(this.etherSwap.filters.Claim(), startHeight), + this.etherSwap.queryFilter(this.etherSwap.filters.Refund(), startHeight), + ]); for (const event of etherLockups) { this.emit( 'eth.lockup', - event.transactionHash, + await event.getTransaction(), formatEtherSwapValues(event.args!), ); } - etherClaims.forEach((event) => { + for (const event of etherClaims) { this.emit( 'eth.claim', event.transactionHash, parseBuffer(event.topics[1]), parseBuffer(event.args!.preimage), ); - }); + } - etherRefunds.forEach((event) => { + for (const event of etherRefunds) { this.emit( 'eth.refund', event.transactionHash, parseBuffer(event.topics[1]), ); - }); - - const erc20Lockups = await this.erc20Swap.queryFilter( - this.erc20Swap.filters.Lockup(), - startHeight, - ); - - const erc20Claims = await this.erc20Swap.queryFilter( - this.erc20Swap.filters.Claim(), - startHeight, - ); + } - const erc20Refunds = await this.erc20Swap.queryFilter( - this.erc20Swap.filters.Refund(), - startHeight, - ); + const [erc20Lockups, erc20Claims, erc20Refunds] = await Promise.all([ + this.erc20Swap.queryFilter(this.erc20Swap.filters.Lockup(), startHeight), + this.erc20Swap.queryFilter(this.erc20Swap.filters.Claim(), startHeight), + this.erc20Swap.queryFilter(this.erc20Swap.filters.Refund(), startHeight), + ]); for (const event of erc20Lockups) { this.emit( 'erc20.lockup', - event.transactionHash, + await event.getTransaction(), formatERC20SwapValues(event.args!), ); } - erc20Claims.forEach((event) => { + for (const event of erc20Claims) { this.emit( 'erc20.claim', event.transactionHash, parseBuffer(event.topics[1]), parseBuffer(event.args!.preimage), ); - }); + } - erc20Refunds.forEach((event) => { + for (const event of erc20Refunds) { this.emit( 'erc20.refund', event.transactionHash, parseBuffer(event.topics[1]), ); - }); + } }; private subscribeContractEvents = async () => { @@ -207,7 +190,7 @@ class ContractEventHandler timelock: bigint, event: ContractEventPayload, ) => { - this.emit('eth.lockup', event.log.transactionHash, { + this.emit('eth.lockup', await event.log.getTransaction(), { amount, claimAddress, refundAddress, @@ -251,7 +234,7 @@ class ContractEventHandler timelock: bigint, event: ContractEventPayload, ) => { - this.emit('erc20.lockup', event.log.transactionHash, { + this.emit('erc20.lockup', await event.log.getTransaction(), { amount, tokenAddress, claimAddress, diff --git a/test/integration/service/Blocks.spec.ts b/test/integration/service/Blocks.spec.ts new file mode 100644 index 00000000..3914df3d --- /dev/null +++ b/test/integration/service/Blocks.spec.ts @@ -0,0 +1,47 @@ +import http from 'http'; +import Logger from '../../../lib/Logger'; +import Blocks from '../../../lib/service/Blocks'; + +describe('Blocks', () => { + const addresses = ['bc1', '123']; + + const server = http.createServer((_, res) => { + res.setHeader('Content-Type', 'application/json'); + res.writeHead(200); + res.end(JSON.stringify(addresses)); + }); + + let blocks: Blocks; + + beforeAll(async () => { + const port = await new Promise((resolve) => { + server.listen(0, () => { + resolve((server.address()! as any).port); + }); + }); + + blocks = new Blocks(Logger.disabledLogger, { + urls: [`http://127.0.0.1:${port}`], + }); + }); + + afterAll(() => { + server.close(); + }); + + test('should update blocks', async () => { + expect(blocks['blocked'].size).toEqual(0); + await blocks.updateBlocks(); + + expect(blocks['blocked'].size).toEqual(addresses.length); + }); + + test.each` + address | blocked + ${addresses[0]} | ${true} + ${addresses[1]} | ${true} + ${'notBlocked'} | ${false} + `('should check if address $address is blocked', ({ address, blocked }) => { + expect(blocks.isBlocked(address)).toEqual(blocked); + }); +}); diff --git a/test/integration/wallet/ethereum/ContractEventHandler.spec.ts b/test/integration/wallet/ethereum/ContractEventHandler.spec.ts index 07d37c82..236162d1 100644 --- a/test/integration/wallet/ethereum/ContractEventHandler.spec.ts +++ b/test/integration/wallet/ethereum/ContractEventHandler.spec.ts @@ -7,7 +7,6 @@ import { MaxUint256 } from 'ethers'; import Logger from '../../../../lib/Logger'; import ContractEventHandler from '../../../../lib/wallet/ethereum/ContractEventHandler'; import { Ethereum } from '../../../../lib/wallet/ethereum/EvmNetworks'; -import { waitForFunctionToBeTrue } from '../../../Utils'; import { EthereumSetup, fundSignerWallet, @@ -15,322 +14,333 @@ import { getSigner, } from '../EthereumTools'; -describe('ContractEventHandler', () => { - let setup: EthereumSetup; - - let etherSwap: EtherSwap; - let erc20Swap: ERC20Swap; - let tokenContract: ERC20; +type Transactions = { + lockup?: string; + claim?: string; + refund?: string; +}; +describe('ContractEventHandler', () => { const contractEventHandler = new ContractEventHandler(Logger.disabledLogger); - let startingHeight: number; - - let eventsEmitted = 0; - const preimage = randomBytes(32); - const preimageHash = crypto.sha256(preimage); - - const etherSwapValues = { - amount: BigInt(1), - claimAddress: '', - timelock: 2, - }; - - const erc20SwapValues = { - amount: BigInt(1), - tokenAddress: '', - claimAddress: '', - timelock: 3, - }; - - const etherSwapTransactionHashes = { - lockup: '', - claim: '', - refund: '', - }; - - const erc20SwapTransactionHashes = { - lockup: '', - claim: '', - refund: '', + const timelock = 1; + const amount = BigInt(123_321); + + const transactions: { + etherSwap: Transactions; + erc20Swap: Transactions; + } = { + etherSwap: {}, + erc20Swap: {}, }; - const registerEtherSwapListeners = () => { - contractEventHandler.once( - 'eth.lockup', - async (transactionHash, emittedEtherSwapValues) => { - await waitForFunctionToBeTrue(() => { - return etherSwapTransactionHashes.lockup !== ''; - }); - - expect(transactionHash).toEqual(etherSwapTransactionHashes.lockup); - expect(emittedEtherSwapValues).toEqual({ - preimageHash, - amount: etherSwapValues.amount, - timelock: etherSwapValues.timelock, - refundAddress: await setup.signer.getAddress(), - claimAddress: etherSwapValues.claimAddress, - }); - - eventsEmitted += 1; - }, - ); - - contractEventHandler.once( - 'eth.claim', - async (transactionHash, emittedPreimageHash, emittedPreimage) => { - await waitForFunctionToBeTrue(() => { - return etherSwapTransactionHashes.claim !== ''; - }); - - expect(transactionHash).toEqual(etherSwapTransactionHashes.claim); - expect(emittedPreimageHash).toEqual(preimageHash); - expect(preimage).toEqual(emittedPreimage); - - eventsEmitted += 1; - }, - ); - - contractEventHandler.once( - 'eth.refund', - async (transactionHash, emittedPreimageHash) => { - await waitForFunctionToBeTrue(() => { - return etherSwapTransactionHashes.refund !== ''; - }); - - expect(transactionHash).toEqual(etherSwapTransactionHashes.refund); - expect(emittedPreimageHash).toEqual(preimageHash); - - eventsEmitted += 1; - }, - ); + let setup: EthereumSetup; + let contracts: { + token: ERC20; + etherSwap: EtherSwap; + erc20Swap: ERC20Swap; }; - const registerErc20SwapListeners = () => { - contractEventHandler.once( - 'erc20.lockup', - async (transactionHash, emittedErc20SwapValues) => { - await waitForFunctionToBeTrue(() => { - return erc20SwapTransactionHashes.lockup !== ''; - }); - - expect(transactionHash).toEqual(erc20SwapTransactionHashes.lockup); - expect(emittedErc20SwapValues).toEqual({ - preimageHash, - amount: erc20SwapValues.amount, - timelock: erc20SwapValues.timelock, - tokenAddress: await tokenContract.getAddress(), - refundAddress: await setup.signer.getAddress(), - claimAddress: erc20SwapValues.claimAddress, - }); - - eventsEmitted += 1; - }, - ); - - contractEventHandler.once( - 'erc20.claim', - async (transactionHash, emittedPreimageHash, emittedPreimage) => { - await waitForFunctionToBeTrue(() => { - return erc20SwapTransactionHashes.claim !== ''; - }); - - expect(transactionHash).toEqual(erc20SwapTransactionHashes.claim); - expect(preimage).toEqual(emittedPreimage); - expect(emittedPreimageHash).toEqual(preimageHash); - - eventsEmitted += 1; - }, - ); - - contractEventHandler.once( - 'erc20.refund', - async (transactionHash, emittedPreimageHash) => { - await waitForFunctionToBeTrue(() => { - return erc20SwapTransactionHashes.refund !== ''; - }); - - expect(transactionHash).toEqual(erc20SwapTransactionHashes.refund); - expect(emittedPreimageHash).toEqual(preimageHash); - - eventsEmitted += 1; - }, - ); - }; + let startingHeight: number; beforeAll(async () => { setup = await getSigner(); - const contracts = await getContracts(setup.signer); - - etherSwap = contracts.etherSwap; - erc20Swap = contracts.erc20Swap; - tokenContract = contracts.token; - - erc20SwapValues.tokenAddress = await tokenContract.getAddress(); + contracts = await getContracts(setup.signer); startingHeight = (await setup.provider.getBlockNumber()) + 1; - etherSwapValues.claimAddress = await setup.etherBase.getAddress(); - erc20SwapValues.claimAddress = await setup.etherBase.getAddress(); - - await fundSignerWallet(setup.signer, setup.etherBase, tokenContract); + await Promise.all([ + contracts.token.approve(contracts.erc20Swap.getAddress(), MaxUint256), + fundSignerWallet(setup.signer, setup.etherBase, contracts.token), + ]); }); - beforeEach(() => { - eventsEmitted = 0; + afterAll(() => { + contractEventHandler.removeAllListeners(); + setup.provider.destroy(); }); test('should init', async () => { - await contractEventHandler.init(Ethereum, etherSwap, erc20Swap); + await contractEventHandler.init( + Ethereum, + contracts.etherSwap, + contracts.erc20Swap, + ); }); - test('should subscribe to the Ethereum Swap contract events', async () => { - registerEtherSwapListeners(); - - // Lockup - let lockupTransaction = await etherSwap.lock( - preimageHash, - etherSwapValues.claimAddress, - etherSwapValues.timelock, + test('should listen to EtherSwap lockup events', async () => { + const tx = await contracts.etherSwap.lock( + crypto.sha256(preimage), + await setup.etherBase.getAddress(), + timelock, { - value: etherSwapValues.amount, + value: amount, }, ); - etherSwapTransactionHashes.lockup = lockupTransaction.hash; - - await lockupTransaction.wait(1); - - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 1; + transactions.etherSwap.lockup = tx.hash; + + const lockupPromise = new Promise((resolve) => { + contractEventHandler.once('eth.lockup', async (transaction, values) => { + expect(transaction).toEqual( + await setup.provider.getTransaction(tx.hash), + ); + expect(values).toEqual({ + amount, + timelock, + preimageHash: crypto.sha256(preimage), + refundAddress: await setup.signer.getAddress(), + claimAddress: await setup.etherBase.getAddress(), + }); + resolve(); + }); }); - // Claim - const claimTransaction = await ( - etherSwap.connect(setup.etherBase) as EtherSwap - ).claim( - preimage, - etherSwapValues.amount, - await setup.signer.getAddress(), - etherSwapValues.timelock, - ); - etherSwapTransactionHashes.claim = claimTransaction.hash; + await tx.wait(1); - await claimTransaction.wait(1); + await lockupPromise; + }); - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 2; + test('should listen to EtherSwap claim events', async () => { + const tx = await contracts.etherSwap + .connect(setup.etherBase) + .claim(preimage, amount, await setup.signer.getAddress(), timelock); + transactions.etherSwap.claim = tx.hash; + + const claimPromise = new Promise((resolve) => { + contractEventHandler.once( + 'eth.claim', + async (transactionHash, preimageHash, preimage) => { + expect(transactionHash).toEqual(tx.hash); + expect(preimageHash).toEqual(preimageHash); + expect(preimage).toEqual(preimage); + resolve(); + }, + ); }); - // Refund - lockupTransaction = await etherSwap.lock( - preimageHash, - etherSwapValues.claimAddress, - etherSwapValues.timelock, + await tx.wait(1); + + await claimPromise; + }); + + test('should listen to EtherSwap refund events', async () => { + const claimTx = await contracts.etherSwap.lock( + crypto.sha256(preimage), + await setup.etherBase.getAddress(), + timelock, { - value: etherSwapValues.amount, + value: amount, }, ); + await claimTx.wait(1); - await lockupTransaction.wait(1); - - const refundTransaction = await etherSwap.refund( - preimageHash, - etherSwapValues.amount, - etherSwapValues.claimAddress, - etherSwapValues.timelock, + const tx = await contracts.etherSwap.refund( + crypto.sha256(preimage), + amount, + await setup.etherBase.getAddress(), + timelock, ); - etherSwapTransactionHashes.refund = refundTransaction.hash; - - await refundTransaction.wait(1); - - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 3; + transactions.etherSwap.refund = tx.hash; + + const refundPromise = new Promise((resolve) => { + contractEventHandler.once( + 'eth.refund', + async (transactionHash, preimageHash) => { + expect(transactionHash).toEqual(tx.hash); + expect(preimageHash).toEqual(preimageHash); + resolve(); + }, + ); }); - }); - test('should subscribe to the ERC20 Swap contract events', async () => { - registerErc20SwapListeners(); + await tx.wait(1); - await ( - await tokenContract.approve(await erc20Swap.getAddress(), MaxUint256) - ).wait(1); + await refundPromise; + }); - // Lockup - let lockupTransaction = await erc20Swap.lock( - preimageHash, - erc20SwapValues.amount, - erc20SwapValues.tokenAddress, - erc20SwapValues.claimAddress, - erc20SwapValues.timelock, + test('should listen to ERC20Swap lockup events', async () => { + const tx = await contracts.erc20Swap.lock( + crypto.sha256(preimage), + amount, + await contracts.token.getAddress(), + await setup.etherBase.getAddress(), + timelock, ); - erc20SwapTransactionHashes.lockup = lockupTransaction.hash; - - await lockupTransaction.wait(1); - - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 1; + transactions.erc20Swap.lockup = tx.hash; + + const lockupPromise = new Promise((resolve) => { + contractEventHandler.once('erc20.lockup', async (transaction, values) => { + expect(transaction).toEqual( + await setup.provider.getTransaction(tx.hash), + ); + expect(values).toEqual({ + amount, + timelock, + preimageHash: crypto.sha256(preimage), + refundAddress: await setup.signer.getAddress(), + claimAddress: await setup.etherBase.getAddress(), + tokenAddress: await contracts.token.getAddress(), + }); + resolve(); + }); }); - // Claim - const claimTransaction = await ( - erc20Swap.connect(setup.etherBase) as ERC20Swap - ).claim( - preimage, - erc20SwapValues.amount, - erc20SwapValues.tokenAddress, - await setup.signer.getAddress(), - erc20SwapValues.timelock, - ); - erc20SwapTransactionHashes.claim = claimTransaction.hash; + await tx.wait(1); - await claimTransaction.wait(1); + await lockupPromise; + }); - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 2; + test('should listen to ERC20Swap claim events', async () => { + const tx = await contracts.erc20Swap + .connect(setup.etherBase) + .claim( + preimage, + amount, + await contracts.token.getAddress(), + await setup.signer.getAddress(), + timelock, + ); + transactions.erc20Swap.claim = tx.hash; + + const claimPromise = new Promise((resolve) => { + contractEventHandler.once( + 'erc20.claim', + async (transactionHash, preimageHash, preimage) => { + expect(transactionHash).toEqual(tx.hash); + expect(preimageHash).toEqual(preimageHash); + expect(preimage).toEqual(preimage); + resolve(); + }, + ); }); - // Refund - lockupTransaction = await erc20Swap.lock( - preimageHash, - erc20SwapValues.amount, - erc20SwapValues.tokenAddress, - erc20SwapValues.claimAddress, - erc20SwapValues.timelock, + await tx.wait(1); + + await claimPromise; + }); + + test('should listen to ERC20 refund events', async () => { + const claimTx = await contracts.erc20Swap.lock( + crypto.sha256(preimage), + amount, + await contracts.token.getAddress(), + await setup.etherBase.getAddress(), + timelock, ); - await lockupTransaction.wait(1); - - const refundTransaction = await erc20Swap.refund( - preimageHash, - erc20SwapValues.amount, - erc20SwapValues.tokenAddress, - erc20SwapValues.claimAddress, - erc20SwapValues.timelock, + await claimTx.wait(1); + + const tx = await contracts.erc20Swap.refund( + crypto.sha256(preimage), + amount, + await contracts.token.getAddress(), + await setup.etherBase.getAddress(), + timelock, ); - erc20SwapTransactionHashes.refund = refundTransaction.hash; + transactions.erc20Swap.refund = tx.hash; + + const refundPromise = new Promise((resolve) => { + contractEventHandler.once( + 'erc20.refund', + async (transactionHash, preimageHash) => { + expect(transactionHash).toEqual(tx.hash); + expect(preimageHash).toEqual(preimageHash); + resolve(); + }, + ); + }); - await refundTransaction.wait(1); + await tx.wait(1); - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 3; + await refundPromise; + }); + + test('should rescan EtherSwap', async () => { + const lockupPromise = new Promise((resolve) => { + contractEventHandler.once('eth.lockup', async (transaction, values) => { + expect(transaction).toEqual( + await setup.provider.getTransaction(transactions.etherSwap.lockup!), + ); + expect(values).toEqual({ + amount, + timelock, + preimageHash: crypto.sha256(preimage), + refundAddress: await setup.signer.getAddress(), + claimAddress: await setup.etherBase.getAddress(), + }); + resolve(); + }); }); - await tokenContract.approve(await erc20Swap.getAddress(), 0); - }); + const claimPromise = new Promise((resolve) => { + contractEventHandler.once( + 'eth.claim', + async (transactionHash, preimageHash, preimage) => { + expect(transactionHash).toEqual(transactions.etherSwap.claim); + expect(preimageHash).toEqual(preimageHash); + expect(preimage).toEqual(preimage); + resolve(); + }, + ); + }); - test('should rescan', async () => { - registerEtherSwapListeners(); - registerErc20SwapListeners(); + const refundPromise = new Promise((resolve) => { + contractEventHandler.once( + 'eth.refund', + async (transactionHash, preimageHash) => { + expect(transactionHash).toEqual(transactions.etherSwap.refund); + expect(preimageHash).toEqual(preimageHash); + resolve(); + }, + ); + }); await contractEventHandler.rescan(startingHeight); + await Promise.all([lockupPromise, claimPromise, refundPromise]); + }); - await waitForFunctionToBeTrue(() => { - return eventsEmitted === 6; + test('should rescan ERC20Swap', async () => { + const lockupPromise = new Promise((resolve) => { + contractEventHandler.once('erc20.lockup', async (transaction, values) => { + expect(transaction).toEqual( + await setup.provider.getTransaction(transactions.erc20Swap.lockup!), + ); + expect(values).toEqual({ + amount, + timelock, + preimageHash: crypto.sha256(preimage), + refundAddress: await setup.signer.getAddress(), + claimAddress: await setup.etherBase.getAddress(), + tokenAddress: await contracts.token.getAddress(), + }); + resolve(); + }); }); - }); - afterAll(() => { - contractEventHandler.removeAllListeners(); - setup.provider.destroy(); + const claimPromise = new Promise((resolve) => { + contractEventHandler.once( + 'erc20.claim', + async (transactionHash, preimageHash, preimage) => { + expect(transactionHash).toEqual(transactions.erc20Swap.claim); + expect(preimageHash).toEqual(preimageHash); + expect(preimage).toEqual(preimage); + resolve(); + }, + ); + }); + + const refundPromise = new Promise((resolve) => { + contractEventHandler.once( + 'erc20.refund', + async (transactionHash, preimageHash) => { + expect(transactionHash).toEqual(transactions.erc20Swap.refund); + expect(preimageHash).toEqual(preimageHash); + resolve(); + }, + ); + }); + + await contractEventHandler.rescan(startingHeight); + await Promise.all([lockupPromise, claimPromise, refundPromise]); }); }); diff --git a/test/unit/swap/EthereumNursery.spec.ts b/test/unit/swap/EthereumNursery.spec.ts index 17483c4f..4e2f5443 100644 --- a/test/unit/swap/EthereumNursery.spec.ts +++ b/test/unit/swap/EthereumNursery.spec.ts @@ -11,6 +11,7 @@ import { ERC20SwapValues, EtherSwapValues } from '../../../lib/consts/Types'; import Swap from '../../../lib/db/models/Swap'; import ReverseSwapRepository from '../../../lib/db/repositories/ReverseSwapRepository'; import SwapRepository from '../../../lib/db/repositories/SwapRepository'; +import Blocks from '../../../lib/service/Blocks'; import Errors from '../../../lib/swap/Errors'; import EthereumNursery from '../../../lib/swap/EthereumNursery'; import Wallet from '../../../lib/wallet/Wallet'; @@ -29,11 +30,11 @@ type claimCallback = ( ) => void; type ethLockupCallback = ( - transactionHash: string, + transaction: any, etherSwapValues: EtherSwapValues, ) => void; type erc20LockupCallback = ( - transactionHash: string, + transaction: any, erc20SwapValues: ERC20SwapValues, ) => void; @@ -193,8 +194,13 @@ const examplePreimage = getHexBuffer( ); const examplePreimageHash = crypto.sha256(examplePreimage); -const exampleTransactionHash = - '0x193be8365ec997f97156dbd894d446135eca8cfbfe3417404c50f32015ee5bb2'; +const exampleTransaction = { + hash: '0x193be8365ec997f97156dbd894d446135eca8cfbfe3417404c50f32015ee5bb2', +}; + +const blocks = { + isBlocked: jest.fn().mockImplementation((addr) => addr === 'blocked'), +} as unknown as Blocks; describe('EthereumNursery', () => { const nursery = new EthereumNursery( @@ -207,6 +213,7 @@ describe('EthereumNursery', () => { ]), } as any, new MockedEthereumManager(), + blocks, ); beforeEach(() => { @@ -266,7 +273,7 @@ describe('EthereumNursery', () => { const newWaitPromise = () => { return { - hash: exampleTransactionHash, + hash: exampleTransaction, wait: jest.fn().mockReturnValue( new Promise((promiseResolve, promiseReject) => { resolve = promiseResolve; @@ -283,7 +290,7 @@ describe('EthereumNursery', () => { expect(reverseSwap).toEqual({ status: SwapUpdateEvent.TransactionConfirmed, }); - expect(transactionHash).toEqual(exampleTransactionHash); + expect(transactionHash).toEqual(exampleTransaction); eventsEmitted += 1; }); @@ -335,13 +342,13 @@ describe('EthereumNursery', () => { } as any; nursery.once('eth.lockup', (_, transactionHash, etherSwapValues) => { - expect(transactionHash).toEqual(exampleTransactionHash); + expect(transactionHash).toEqual(exampleTransaction.hash); expect(etherSwapValues).toEqual(suppliedEtherSwapValues); lockupEmitted = true; }); - await emitEthLockup(exampleTransactionHash, suppliedEtherSwapValues); + await emitEthLockup(exampleTransaction, suppliedEtherSwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(1); expect(mockGetSwap).toHaveBeenCalledWith({ @@ -354,7 +361,7 @@ describe('EthereumNursery', () => { expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); expect(mockSetLockupTransaction).toHaveBeenCalledWith( mockGetSwapResult, - exampleTransactionHash, + exampleTransaction.hash, 10, true, ); @@ -383,7 +390,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitEthLockup(exampleTransactionHash, suppliedEtherSwapValues); + await emitEthLockup(exampleTransaction, suppliedEtherSwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(1); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); @@ -405,7 +412,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitEthLockup(exampleTransactionHash, suppliedEtherSwapValues); + await emitEthLockup(exampleTransaction, suppliedEtherSwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(2); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(2); @@ -424,7 +431,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitEthLockup(exampleTransactionHash, suppliedEtherSwapValues); + await emitEthLockup(exampleTransaction, suppliedEtherSwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(3); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(3); @@ -450,7 +457,7 @@ describe('EthereumNursery', () => { // Chain currency is not Ether mockGetSwapResult.orderSide = OrderSide.BUY; - await emitEthLockup(exampleTransactionHash, suppliedEtherSwapValues); + await emitEthLockup(exampleTransaction, suppliedEtherSwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(1); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(0); @@ -461,7 +468,7 @@ describe('EthereumNursery', () => { // No suitable Swap in database mockGetSwapResult = null; - await emitEthLockup(exampleTransactionHash, suppliedEtherSwapValues); + await emitEthLockup(exampleTransaction, suppliedEtherSwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(2); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(0); @@ -470,6 +477,31 @@ describe('EthereumNursery', () => { expect(lockupFailed).toEqual(0); }); + test('should reject EtherSwap lockup transaction from blocked addresses', async () => { + const lockupPromise = new Promise((resolve) => { + nursery.once('lockup.failed', (_, error) => { + expect(error).toEqual(Errors.BLOCKED_ADDRESS().message); + resolve(); + }); + }); + + mockGetSwapResult = { + pair: 'ETH/BTC', + expectedAmount: 10, + orderSide: OrderSide.SELL, + timeoutBlockHeight: 11102219, + }; + + emitEthLockup({ ...exampleTransaction, from: 'blocked' }, { + claimAddress: mockAddress, + amount: BigInt('100000000000'), + preimageHash: getHexString(examplePreimageHash), + timelock: mockGetSwapResult.timeoutBlockHeight, + } as any); + + await lockupPromise; + }); + test('should listen to EtherSwap claim events', async () => { let emittedEvents = 0; @@ -485,7 +517,7 @@ describe('EthereumNursery', () => { }); await emitEthClaim( - exampleTransactionHash, + exampleTransaction.hash, examplePreimageHash, examplePreimage, ); @@ -504,7 +536,7 @@ describe('EthereumNursery', () => { mockGetReverseSwapResult = null; await emitEthClaim( - exampleTransactionHash, + exampleTransaction.hash, examplePreimageHash, examplePreimage, ); @@ -534,13 +566,13 @@ describe('EthereumNursery', () => { } as any; nursery.once('erc20.lockup', (_, transactionHash, erc20SwapValues) => { - expect(transactionHash).toEqual(exampleTransactionHash); + expect(transactionHash).toEqual(exampleTransaction.hash); expect(erc20SwapValues).toEqual(suppliedERC20SwapValues); lockupEmitted = true; }); - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(1); expect(mockGetSwap).toHaveBeenCalledWith({ @@ -558,7 +590,7 @@ describe('EthereumNursery', () => { expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); expect(mockSetLockupTransaction).toHaveBeenCalledWith( mockGetSwapResult, - exampleTransactionHash, + exampleTransaction.hash, 10, true, ); @@ -594,7 +626,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(1); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); @@ -616,7 +648,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(2); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(2); @@ -638,7 +670,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(3); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(3); @@ -657,7 +689,7 @@ describe('EthereumNursery', () => { lockupFailed += 1; }); - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(4); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(4); @@ -685,7 +717,7 @@ describe('EthereumNursery', () => { // Chain currency is not a token mockGetSwapResult.orderSide = OrderSide.SELL; - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(1); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(0); @@ -696,7 +728,7 @@ describe('EthereumNursery', () => { // No suitable Swap in database mockGetSwapResult = null; - await emitErc20Lockup(exampleTransactionHash, suppliedERC20SwapValues); + await emitErc20Lockup(exampleTransaction, suppliedERC20SwapValues); expect(mockGetSwap).toHaveBeenCalledTimes(2); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(0); @@ -705,6 +737,32 @@ describe('EthereumNursery', () => { expect(lockupFailed).toEqual(0); }); + test('should reject ERC20Swap lockup transaction from blocked addresses', async () => { + const lockupPromise = new Promise((resolve) => { + nursery.once('lockup.failed', (_, error) => { + expect(error).toEqual(Errors.BLOCKED_ADDRESS().message); + resolve(); + }); + }); + + mockGetSwapResult = { + pair: 'BTC/USDT', + expectedAmount: 10, + orderSide: OrderSide.BUY, + timeoutBlockHeight: 11102222, + }; + + emitErc20Lockup({ ...exampleTransaction, from: 'blocked' }, { + claimAddress: mockAddress, + amount: BigInt('1000'), + tokenAddress: mockTokenAddress, + timelock: mockGetSwapResult.timeoutBlockHeight, + preimageHash: getHexString(examplePreimageHash), + } as any); + + await lockupPromise; + }); + test('should listen to ERC20Swap claim events', async () => { let emittedEvents = 0; @@ -720,7 +778,7 @@ describe('EthereumNursery', () => { }); await emitErc20Claim( - exampleTransactionHash, + exampleTransaction.hash, examplePreimageHash, examplePreimage, ); @@ -739,7 +797,7 @@ describe('EthereumNursery', () => { mockGetReverseSwapResult = null; await emitErc20Claim( - exampleTransactionHash, + exampleTransaction.hash, examplePreimageHash, examplePreimage, ); diff --git a/test/unit/swap/SwapManager.spec.ts b/test/unit/swap/SwapManager.spec.ts index d35f84e5..4ead3359 100644 --- a/test/unit/swap/SwapManager.spec.ts +++ b/test/unit/swap/SwapManager.spec.ts @@ -29,6 +29,7 @@ import ReverseSwapRepository from '../../../lib/db/repositories/ReverseSwapRepos import SwapRepository from '../../../lib/db/repositories/SwapRepository'; import LndClient from '../../../lib/lightning/LndClient'; import RateProvider from '../../../lib/rates/RateProvider'; +import Blocks from '../../../lib/service/Blocks'; import PaymentRequestUtils from '../../../lib/service/PaymentRequestUtils'; import TimeoutDeltaProvider from '../../../lib/service/TimeoutDeltaProvider'; import Errors from '../../../lib/swap/Errors'; @@ -310,6 +311,10 @@ jest.mock('../../../lib/swap/SwapNursery', () => { })); }); +const blocks = { + isBlocked: jest.fn().mockReturnValue(false), +} as unknown as Blocks; + describe('SwapManager', () => { let manager: SwapManager; @@ -329,6 +334,11 @@ describe('SwapManager', () => { lndClient: new MockedLndClient(), } as any as Currency; + const rbtcCurrency = { + symbol: 'RBTC', + type: CurrencyType.Ether, + } as any as Currency; + beforeEach(() => { jest.clearAllMocks(); @@ -363,10 +373,12 @@ describe('SwapManager', () => { {} as PaymentRequestUtils, new SwapOutputType(OutputType.Compatibility), 0, + blocks, ); manager['currencies'].set(btcCurrency.symbol, btcCurrency); manager['currencies'].set(ltcCurrency.symbol, ltcCurrency); + manager['currencies'].set(rbtcCurrency.symbol, rbtcCurrency); }); afterAll(() => { @@ -401,7 +413,7 @@ describe('SwapManager', () => { await manager.init([btcCurrency, ltcCurrency], []); - expect(manager.currencies.size).toEqual(2); + expect(manager.currencies.size).toEqual(3); expect(manager.currencies.get('BTC')).toEqual(btcCurrency); expect(manager.currencies.get('LTC')).toEqual(ltcCurrency); @@ -1169,6 +1181,24 @@ describe('SwapManager', () => { ).rejects.toEqual(Errors.NO_LIGHTNING_SUPPORT(notFoundSymbol)); }); + test('should throw when creating reverse swaps to blocked addresses', async () => { + const claimAddress = '0x123'; + blocks.isBlocked = jest.fn().mockReturnValue(true); + + await expect( + manager.createReverseSwap({ + claimAddress, + orderSide: OrderSide.SELL, + baseCurrency: btcCurrency.symbol, + quoteCurrency: rbtcCurrency.symbol, + } as any), + ).rejects.toEqual(Errors.BLOCKED_ADDRESS()); + expect(blocks.isBlocked).toHaveBeenCalledTimes(1); + expect(blocks.isBlocked).toHaveBeenCalledWith(claimAddress); + + blocks.isBlocked = jest.fn().mockReturnValue(false); + }); + test('should recreate filters', () => { const recreateFilters = manager['recreateFilters']; diff --git a/test/unit/swap/UtxoNursery.spec.ts b/test/unit/swap/UtxoNursery.spec.ts index 4648de9f..953c4fa6 100644 --- a/test/unit/swap/UtxoNursery.spec.ts +++ b/test/unit/swap/UtxoNursery.spec.ts @@ -20,6 +20,7 @@ import { } from '../../../lib/consts/Enums'; import ReverseSwapRepository from '../../../lib/db/repositories/ReverseSwapRepository'; import SwapRepository from '../../../lib/db/repositories/SwapRepository'; +import Blocks from '../../../lib/service/Blocks'; import Errors from '../../../lib/swap/Errors'; import UtxoNursery from '../../../lib/swap/UtxoNursery'; import Wallet from '../../../lib/wallet/Wallet'; @@ -172,9 +173,17 @@ describe('UtxoNursery', () => { const btcWallet = new MockedWallet(); const btcChainClient = new MockedChainClient('BTC'); - const nursery = new UtxoNursery(Logger.disabledLogger, { - wallets: new Map([['BTC', btcWallet]]), - } as any); + const blocks = { + isBlocked: jest.fn().mockReturnValue(false), + } as unknown as Blocks; + + const nursery = new UtxoNursery( + Logger.disabledLogger, + { + wallets: new Map([['BTC', btcWallet]]), + } as any, + blocks, + ); beforeAll(async () => { await setup(); @@ -253,7 +262,7 @@ describe('UtxoNursery', () => { }, }); - expect(mockEncodeAddress).toHaveBeenCalledTimes(1); + expect(mockEncodeAddress).toHaveBeenCalledTimes(4); expect(mockEncodeAddress).toHaveBeenCalledWith(transaction.outs[0].script); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); @@ -396,7 +405,7 @@ describe('UtxoNursery', () => { await checkSwapOutputs(btcChainClient, btcWallet, transaction, false); expect(mockGetSwap).toHaveBeenCalledTimes(1); - expect(mockEncodeAddress).toHaveBeenCalledTimes(1); + expect(mockEncodeAddress).toHaveBeenCalledTimes(4); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); expect(mockRemoveOutputFilter).toHaveBeenCalledTimes(1); @@ -425,7 +434,7 @@ describe('UtxoNursery', () => { expect(eventEmitted).toEqual(true); expect(mockGetSwap).toHaveBeenCalledTimes(1); - expect(mockEncodeAddress).toHaveBeenCalledTimes(1); + expect(mockEncodeAddress).toHaveBeenCalledTimes(4); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); expect(mockRemoveOutputFilter).toHaveBeenCalledTimes(0); @@ -456,7 +465,7 @@ describe('UtxoNursery', () => { expect(eventEmitted).toEqual(true); expect(mockGetSwap).toHaveBeenCalledTimes(1); - expect(mockEncodeAddress).toHaveBeenCalledTimes(1); + expect(mockEncodeAddress).toHaveBeenCalledTimes(4); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); expect(mockRemoveOutputFilter).toHaveBeenCalledTimes(0); @@ -483,10 +492,10 @@ describe('UtxoNursery', () => { expect(eventEmitted).toEqual(true); expect(mockGetSwap).toHaveBeenCalledTimes(1); - expect(mockEncodeAddress).toHaveBeenCalledTimes(1); + expect(mockEncodeAddress).toHaveBeenCalledTimes(4); expect(mockSetLockupTransaction).toHaveBeenCalledTimes(1); expect(mockEstimateFee).toHaveBeenCalledTimes(1); - expect(mockGetRawTransaction).toHaveBeenCalledTimes(2); + expect(mockGetRawTransaction).toHaveBeenCalledTimes(4); expect(mockGetRawTransaction).toHaveBeenNthCalledWith( 1, 'a21b0b3763a64ce2e5da23c52e3496c70c2b3268a37633653e21325ba64d4056', @@ -554,6 +563,41 @@ describe('UtxoNursery', () => { expect(mockGetKeysByIndex).toHaveBeenCalledWith(mockGetSwapResult.keyIndex); }); + test('should reject transactions from blocked addresses', async () => { + const checkSwapOutputs = nursery['checkSwapOutputs']; + + const transaction = Transaction.fromHex(sampleTransactions.lockup); + transaction.ins[0].sequence = 0xffffffff; + transaction.ins[1].sequence = 0xffffffff; + + mockGetSwapResult = { + id: '0conf', + acceptZeroConf: true, + redeemScript: sampleRedeemScript, + }; + + mockGetRawTransactionVerboseResult = () => ({ + confirmations: 1, + }); + + const failPromise = new Promise((resolve) => { + nursery.once('swap.lockup.failed', (swap, error) => { + expect(swap).toEqual(mockGetSwapResult); + expect(error).toEqual(Errors.BLOCKED_ADDRESS().message); + resolve(); + }); + }); + + blocks.isBlocked = jest.fn().mockReturnValue(true); + await checkSwapOutputs(btcChainClient, btcWallet, transaction, false); + + expect(blocks.isBlocked).toHaveBeenCalledTimes(1); + + blocks.isBlocked = jest.fn().mockReturnValue(false); + + await failPromise; + }); + test('should handle claimed Reverse Swaps', async () => { const checkReverseSwapsClaims = nursery['checkReverseSwapClaims'];