Skip to content

Commit

Permalink
feat: check balance before creating swap (#663)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Sep 10, 2024
1 parent e4568d1 commit ce479fd
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 0 deletions.
20 changes: 20 additions & 0 deletions lib/service/BalanceCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import WalletManager from '../wallet/WalletManager';
import Errors from './Errors';

class BalanceCheck {
constructor(private readonly walletManager: WalletManager) {}

public checkBalance = async (symbol: string, amount: number) => {
const wallet = this.walletManager.wallets.get(symbol);
if (wallet === undefined) {
throw Errors.CURRENCY_NOT_FOUND(symbol);
}

const balance = await wallet.getBalance();
if (balance.confirmedBalance < amount) {
throw Errors.INSUFFICIENT_LIQUIDITY();
}
};
}

export default BalanceCheck;
4 changes: 4 additions & 0 deletions lib/service/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,8 @@ export default {
message: 'invalid quote',
code: concatErrorCode(ErrorCodePrefix.Service, 50),
}),
INSUFFICIENT_LIQUIDITY: (): Error => ({
message: 'insufficient liquidity',
code: concatErrorCode(ErrorCodePrefix.Service, 51),
}),
};
7 changes: 7 additions & 0 deletions lib/service/Renegotiator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
formatERC20SwapValues,
formatEtherSwapValues,
} from '../wallet/ethereum/ContractUtils';
import BalanceCheck from './BalanceCheck';
import Errors from './Errors';
import TimeoutDeltaProvider from './TimeoutDeltaProvider';
import ChainSwapSigner from './cooperative/ChainSwapSigner';
Expand All @@ -38,6 +39,7 @@ class Renegotiator {
private readonly chainSwapSigner: ChainSwapSigner,
private readonly eipSigner: EipSigner,
private readonly rateProvider: RateProvider,
private readonly balanceCheck: BalanceCheck,
) {}

public getQuote = async (swapId: string): Promise<number> => {
Expand All @@ -61,6 +63,11 @@ class Renegotiator {
throw Errors.INVALID_QUOTE();
}

await this.balanceCheck.checkBalance(
swap.sendingData.symbol,
serverLockAmount,
);

this.logger.info(
`Accepted new quote for ${swapTypeToPrettyString(swap.type)} Swap ${swap.id}: ${newQuote}`,
);
Expand Down
10 changes: 10 additions & 0 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { SwapNurseryEvents } from '../swap/PaymentHandler';
import SwapManager, { ChannelCreationInfo } from '../swap/SwapManager';
import SwapOutputType from '../swap/SwapOutputType';
import WalletManager, { Currency } from '../wallet/WalletManager';
import BalanceCheck from './BalanceCheck';
import Blocks from './Blocks';
import ElementsService from './ElementsService';
import Errors from './Errors';
Expand Down Expand Up @@ -144,6 +145,7 @@ class Service {

private prepayMinerFee: boolean;

private balanceCheck: BalanceCheck;
private readonly paymentRequestUtils: PaymentRequestUtils;
private readonly timeoutDeltaProvider: TimeoutDeltaProvider;

Expand Down Expand Up @@ -197,6 +199,7 @@ class Service {
this.rateProvider,
);

this.balanceCheck = new BalanceCheck(this.walletManager);
this.swapManager = new SwapManager(
this.logger,
notifications,
Expand All @@ -215,6 +218,7 @@ class Service {
config.swap,
this.lockupTransactionTracker,
this.sidecar,
this.balanceCheck,
);

this.eventHandler = new EventHandler(
Expand Down Expand Up @@ -1682,6 +1686,7 @@ class Service {
args.version,
SwapType.ReverseSubmarine,
);
await this.balanceCheck.checkBalance(sendingCurrency.symbol, onchainAmount);

let prepayMinerFeeInvoiceAmount: number | undefined = undefined;
let prepayMinerFeeOnchainAmount: number | undefined = undefined;
Expand Down Expand Up @@ -1968,6 +1973,11 @@ class Service {
if (args.serverLockAmount < 1) {
throw Errors.ONCHAIN_AMOUNT_TOO_LOW();
}

await this.balanceCheck.checkBalance(
sendingCurrency.symbol,
args.serverLockAmount,
);
}

const referralId = await this.getReferralId(args.referralId);
Expand Down
3 changes: 3 additions & 0 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import TransactionLabelRepository from '../db/repositories/TransactionLabelRepos
import NotificationClient from '../notifications/NotificationClient';
import LockupTransactionTracker from '../rates/LockupTransactionTracker';
import RateProvider from '../rates/RateProvider';
import BalanceCheck from '../service/BalanceCheck';
import Blocks from '../service/Blocks';
import InvoiceExpiryHelper from '../service/InvoiceExpiryHelper';
import PaymentRequestUtils from '../service/PaymentRequestUtils';
Expand Down Expand Up @@ -181,6 +182,7 @@ class SwapManager {
swapConfig: SwapConfig,
lockupTransactionTracker: LockupTransactionTracker,
sidecar: Sidecar,
balanceCheck: BalanceCheck,
) {
this.deferredClaimer = new DeferredClaimer(
this.logger,
Expand Down Expand Up @@ -227,6 +229,7 @@ class SwapManager {
this.chainSwapSigner,
this.eipSigner,
rateProvider,
balanceCheck,
);

this.reverseRoutingHints = new ReverseRoutingHints(
Expand Down
53 changes: 53 additions & 0 deletions test/integration/service/BalanceCheck.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Logger from '../../../lib/Logger';
import BalanceCheck from '../../../lib/service/BalanceCheck';
import Errors from '../../../lib/service/Errors';
import WalletManager from '../../../lib/wallet/WalletManager';
import CoreWalletProvider from '../../../lib/wallet/providers/CoreWalletProvider';
import { bitcoinClient } from '../Nodes';

jest.mock('../../../lib/db/repositories/ChainTipRepository');

describe('BalanceCheck', () => {
const wallet = new CoreWalletProvider(Logger.disabledLogger, bitcoinClient);
const balanceCheck = new BalanceCheck({
wallets: new Map<string, any>([['BTC', wallet]]),
} as unknown as WalletManager);

beforeAll(async () => {
await bitcoinClient.connect();
});

afterAll(() => {
bitcoinClient.disconnect();
});

test('should throw when no wallet can be found', async () => {
const symbol = 'notFound';
await expect(balanceCheck.checkBalance(symbol, 1)).rejects.toEqual(
Errors.CURRENCY_NOT_FOUND(symbol),
);
});

test('should not throw when amount is less than balance', async () => {
await balanceCheck.checkBalance(
'BTC',
(await wallet.getBalance()).confirmedBalance - 1,
);
});

test('should not throw when amount is equal balance', async () => {
await balanceCheck.checkBalance(
'BTC',
(await wallet.getBalance()).confirmedBalance,
);
});

test('should throw when amount is more than balance', async () => {
await expect(
balanceCheck.checkBalance(
'BTC',
(await wallet.getBalance()).confirmedBalance + 1,
),
).rejects.toEqual(Errors.INSUFFICIENT_LIQUIDITY());
});
});
35 changes: 35 additions & 0 deletions test/integration/service/Renegotiator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../../../lib/consts/Enums';
import ChainSwapRepository from '../../../lib/db/repositories/ChainSwapRepository';
import RateProvider from '../../../lib/rates/RateProvider';
import BalanceCheck from '../../../lib/service/BalanceCheck';
import Errors from '../../../lib/service/Errors';
import Renegotiator from '../../../lib/service/Renegotiator';
import ChainSwapSigner from '../../../lib/service/cooperative/ChainSwapSigner';
Expand Down Expand Up @@ -126,6 +127,10 @@ describe('Renegotiator', () => {
},
} as any as RateProvider;

const balanceCheck = {
checkBalance: jest.fn().mockImplementation(async () => {}),
} as unknown as BalanceCheck;

let negotiator: Renegotiator;

beforeAll(async () => {
Expand Down Expand Up @@ -163,6 +168,7 @@ describe('Renegotiator', () => {
chainSwapSigner,
eipSigner,
rateProvider,
balanceCheck,
);

jest.clearAllMocks();
Expand Down Expand Up @@ -213,6 +219,35 @@ describe('Renegotiator', () => {
);
});

test('should throw when balance check fails', async () => {
const swapId = 'someId';

ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({
receivingData: {
symbol: 'BTC',
amount: 100_000,
},
sendingData: {
symbol: 'BTC',
},
});
negotiator['validateEligibility'] = jest
.fn()
.mockImplementation(async () => {});

balanceCheck.checkBalance = jest.fn().mockImplementation(async () => {
throw Errors.INSUFFICIENT_LIQUIDITY();
});

await expect(negotiator.acceptQuote(swapId, 94_877)).rejects.toEqual(
Errors.INSUFFICIENT_LIQUIDITY(),
);
expect(balanceCheck.checkBalance).toHaveBeenCalledTimes(1);
expect(balanceCheck.checkBalance).toHaveBeenCalledWith('BTC', 94_877);

balanceCheck.checkBalance = jest.fn().mockImplementation(async () => {});
});

describe('UTXO based chain', () => {
test.each`
confirmed
Expand Down
6 changes: 6 additions & 0 deletions test/unit/service/Service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,12 @@ const mockedLndClient = <jest.Mock<LndClient>>(<any>LndClient);

jest.mock('../../../lib/db/repositories/ReverseRoutingHintRepository');

jest.mock('../../../lib/service/BalanceCheck', () => {
return jest.fn().mockImplementation(() => ({
checkBalance: jest.fn().mockImplementation(async () => {}),
}));
});

describe('Service', () => {
const configPairs = [
{
Expand Down

0 comments on commit ce479fd

Please sign in to comment.