From 23f1bbdf01fb7fc1b84b7ebd137e3fd44871d7f5 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Sun, 22 Dec 2024 17:17:12 +0100 Subject: [PATCH] feat: custom fees for referrals --- lib/api/v2/routers/SwapRouter.ts | 34 ++- lib/db/Migration.ts | 28 +- lib/db/models/Referral.ts | 25 ++ lib/db/repositories/ReferralRepository.ts | 8 + lib/rates/FeeProvider.ts | 22 +- lib/rates/providers/RateProviderLegacy.ts | 4 + lib/rates/providers/RateProviderTaproot.ts | 78 ++++- lib/service/Renegotiator.ts | 29 +- lib/service/Service.ts | 78 +++-- lib/swap/PaymentHandler.ts | 2 +- test/integration/db/models/Referral.spec.ts | 43 +++ test/integration/service/Renegotiator.spec.ts | 161 ++++++---- test/unit/api/Utils.ts | 3 +- .../api/v2/routers/ReferralRouter.spec.ts | 17 +- test/unit/api/v2/routers/SwapRouter.spec.ts | 94 ++++-- test/unit/rates/FeeProvider.spec.ts | 277 ++++++++++++------ .../providers/RateProviderLegacy.spec.ts | 14 + .../providers/RateProviderTaproot.spec.ts | 153 +++++++--- test/unit/service/Service.spec.ts | 64 ++-- 19 files changed, 853 insertions(+), 281 deletions(-) create mode 100644 test/integration/db/models/Referral.spec.ts diff --git a/lib/api/v2/routers/SwapRouter.ts b/lib/api/v2/routers/SwapRouter.ts index 434bc1b8..07ecb474 100644 --- a/lib/api/v2/routers/SwapRouter.ts +++ b/lib/api/v2/routers/SwapRouter.ts @@ -3,6 +3,7 @@ import Logger from '../../../Logger'; import { getHexString, stringify } from '../../../Utils'; import { SwapUpdateEvent, SwapVersion } from '../../../consts/Enums'; import ChainSwapRepository from '../../../db/repositories/ChainSwapRepository'; +import ReferralRepository from '../../../db/repositories/ReferralRepository'; import SwapRepository from '../../../db/repositories/SwapRepository'; import RateProviderTaproot from '../../../rates/providers/RateProviderTaproot'; import CountryCodes from '../../../service/CountryCodes'; @@ -1654,13 +1655,17 @@ class SwapRouter extends RouterBase { return router; }; - private getSubmarine = (_req: Request, res: Response) => + private getSubmarine = async (req: Request, res: Response) => { + const referral = await this.getReferralFromHeader(req); successResponse( res, RateProviderTaproot.serializePairs( - this.service.rateProvider.providers[SwapVersion.Taproot].submarinePairs, + this.service.rateProvider.providers[ + SwapVersion.Taproot + ].getSubmarinePairs(referral), ), ); + }; private createSubmarine = async (req: Request, res: Response) => { const { to, from, invoice, webhook, pairHash, refundPublicKey } = @@ -1816,13 +1821,17 @@ class SwapRouter extends RouterBase { successResponse(res, {}); }; - private getReverse = (_req: Request, res: Response) => + private getReverse = async (req: Request, res: Response) => { + const referral = await this.getReferralFromHeader(req); successResponse( res, RateProviderTaproot.serializePairs( - this.service.rateProvider.providers[SwapVersion.Taproot].reversePairs, + this.service.rateProvider.providers[ + SwapVersion.Taproot + ].getReversePairs(referral), ), ); + }; private createReverse = async (req: Request, res: Response) => { const { @@ -1960,13 +1969,17 @@ class SwapRouter extends RouterBase { }); }; - private getChain = (_req: Request, res: Response) => + private getChain = async (req: Request, res: Response) => { + const referral = await this.getReferralFromHeader(req); successResponse( res, RateProviderTaproot.serializePairs( - this.service.rateProvider.providers[SwapVersion.Taproot].chainPairs, + this.service.rateProvider.providers[SwapVersion.Taproot].getChainPairs( + referral, + ), ), ); + }; // TODO: claim covenant private createChain = async (req: Request, res: Response) => { @@ -2247,6 +2260,15 @@ class SwapRouter extends RouterBase { return res; }; + + private getReferralFromHeader = async (req: Request) => { + const referral = req.header('referral'); + if (referral === undefined) { + return null; + } + + return ReferralRepository.getReferralById(referral); + }; } export default SwapRouter; diff --git a/lib/db/Migration.ts b/lib/db/Migration.ts index 5c10ec7c..135b15ba 100644 --- a/lib/db/Migration.ts +++ b/lib/db/Migration.ts @@ -93,7 +93,7 @@ const decodeInvoice = ( // TODO: integration tests for actual migrations class Migration { - private static latestSchemaVersion = 12; + private static latestSchemaVersion = 13; private toBackFill: number[] = []; @@ -574,6 +574,32 @@ class Migration { break; } + case 12: { + await this.sequelize + .getQueryInterface() + .addColumn(Referral.tableName, 'submarinePremium', { + type: new DataTypes.INTEGER(), + allowNull: true, + }); + + await this.sequelize + .getQueryInterface() + .addColumn(Referral.tableName, 'reversePremium', { + type: new DataTypes.INTEGER(), + allowNull: true, + }); + + await this.sequelize + .getQueryInterface() + .addColumn(Referral.tableName, 'chainPremium', { + type: new DataTypes.INTEGER(), + allowNull: true, + }); + + await this.finishMigration(versionRow.version, currencies); + break; + } + default: throw `found unexpected database version ${versionRow.version}`; } diff --git a/lib/db/models/Referral.ts b/lib/db/models/Referral.ts index 02729766..6843007c 100644 --- a/lib/db/models/Referral.ts +++ b/lib/db/models/Referral.ts @@ -1,4 +1,5 @@ import { DataTypes, Model, Sequelize } from 'sequelize'; +import { SwapType } from '../../consts/Enums'; type ReferralType = { id: string; @@ -8,6 +9,10 @@ type ReferralType = { feeShare: number; routingNode?: string; + + submarinePremium?: number; + reversePremium?: number; + chainPremium?: number; }; class Referral extends Model implements ReferralType { @@ -19,6 +24,10 @@ class Referral extends Model implements ReferralType { public feeShare!: number; public routingNode?: string; + public submarinePremium?: number; + public reversePremium?: number; + public chainPremium?: number; + public static load = (sequelize: Sequelize): void => { Referral.init( { @@ -43,6 +52,9 @@ class Referral extends Model implements ReferralType { allowNull: true, unique: true, }, + submarinePremium: { type: new DataTypes.INTEGER(), allowNull: true }, + reversePremium: { type: new DataTypes.INTEGER(), allowNull: true }, + chainPremium: { type: new DataTypes.INTEGER(), allowNull: true }, }, { sequelize, @@ -60,6 +72,19 @@ class Referral extends Model implements ReferralType { }, ); }; + + public premiumForType = (type: SwapType) => { + switch (type) { + case SwapType.Submarine: + return this.submarinePremium; + + case SwapType.ReverseSubmarine: + return this.reversePremium; + + case SwapType.Chain: + return this.chainPremium; + } + }; } export default Referral; diff --git a/lib/db/repositories/ReferralRepository.ts b/lib/db/repositories/ReferralRepository.ts index e604da2b..e24f05cc 100644 --- a/lib/db/repositories/ReferralRepository.ts +++ b/lib/db/repositories/ReferralRepository.ts @@ -42,6 +42,14 @@ ORDER BY year, month; return Referral.findAll(); }; + public static getReferralById = (id: string): Promise => { + return Referral.findOne({ + where: { + id, + }, + }); + }; + public static getReferralByApiKey = ( apiKey: string, ): Promise => { diff --git a/lib/rates/FeeProvider.ts b/lib/rates/FeeProvider.ts index ac740ca5..11e119f4 100644 --- a/lib/rates/FeeProvider.ts +++ b/lib/rates/FeeProvider.ts @@ -20,6 +20,7 @@ import { swapTypeToString, } from '../consts/Enums'; import { PairConfig } from '../consts/Types'; +import Referral from '../db/models/Referral'; import WalletLiquid from '../wallet/WalletLiquid'; import WalletManager from '../wallet/WalletManager'; import { Ethereum, Rsk } from '../wallet/ethereum/EvmNetworks'; @@ -132,6 +133,8 @@ class FeeProvider { }, }; + public static percentFeeDecimals = 2; + private static readonly defaultFee = 1; // A map between the symbols of the pairs and their percentage fees @@ -146,6 +149,16 @@ class FeeProvider { private getFeeEstimation: (symbol: string) => Promise>, ) {} + public static addPremium = (fee: number, premium?: number): number => { + if (premium === null || premium === undefined) { + return fee; + } + + return parseFloat( + (fee + premium / 100).toFixed(FeeProvider.percentFeeDecimals), + ); + }; + public init = (pairs: PairConfig[]): void => { pairs.forEach((pair) => { const pairId = getPairId(pair); @@ -203,6 +216,7 @@ class FeeProvider { orderSide: OrderSide, type: SwapType, feeType: PercentageFeeType = PercentageFeeType.Calculation, + referral: Referral | null, ): number => { const percentages = this.percentageFees.get(pair); if (percentages === undefined) { @@ -210,10 +224,12 @@ class FeeProvider { } const percentageType = percentages[type]; - const percentage = + const percentage = FeeProvider.addPremium( typeof percentageType === 'number' ? percentageType - : percentageType[orderSide]; + : percentageType[orderSide], + referral?.premiumForType(type), + ); return feeType === PercentageFeeType.Calculation ? percentage / 100 @@ -228,6 +244,7 @@ class FeeProvider { amount: number, type: SwapType, feeType: BaseFeeType, + referral: Referral | null, ): { baseFee: number; percentageFee: number; @@ -237,6 +254,7 @@ class FeeProvider { orderSide, type, PercentageFeeType.Calculation, + referral, ); if (percentageFee !== 0) { diff --git a/lib/rates/providers/RateProviderLegacy.ts b/lib/rates/providers/RateProviderLegacy.ts index e2ff9886..bda57a43 100644 --- a/lib/rates/providers/RateProviderLegacy.ts +++ b/lib/rates/providers/RateProviderLegacy.ts @@ -67,12 +67,14 @@ class RateProviderLegacy extends RateProviderBase { OrderSide.BUY, SwapType.Submarine, PercentageFeeType.Display, + null, ), percentage: this.feeProvider.getPercentageFee( id, OrderSide.BUY, SwapType.ReverseSubmarine, PercentageFeeType.Display, + null, ), minerFees: { baseAsset: emptyMinerFees, @@ -98,12 +100,14 @@ class RateProviderLegacy extends RateProviderBase { OrderSide.BUY, SwapType.Submarine, PercentageFeeType.Display, + null, ), percentage: this.feeProvider.getPercentageFee( pairId, OrderSide.BUY, SwapType.ReverseSubmarine, PercentageFeeType.Display, + null, ), minerFees: { baseAsset: this.feeProvider.minerFees.get(base)![SwapVersion.Legacy], diff --git a/lib/rates/providers/RateProviderTaproot.ts b/lib/rates/providers/RateProviderTaproot.ts index 9703ed55..73fb6f0f 100644 --- a/lib/rates/providers/RateProviderTaproot.ts +++ b/lib/rates/providers/RateProviderTaproot.ts @@ -15,6 +15,7 @@ import { SwapVersion, } from '../../consts/Enums'; import { ChainSwapPairConfig, PairConfig } from '../../consts/Types'; +import Referral from '../../db/models/Referral'; import Errors from '../../service/Errors'; import NodeSwitch from '../../swap/NodeSwitch'; import { Currency } from '../../wallet/WalletManager'; @@ -68,17 +69,17 @@ type SwapTypes = | ChainPairTypeTaproot; class RateProviderTaproot extends RateProviderBase { - public readonly submarinePairs = new Map< + private readonly submarinePairs = new Map< string, Map >(); - public readonly reversePairs = new Map< + private readonly reversePairs = new Map< string, Map >(); - public readonly chainPairs = new Map< + private readonly chainPairs = new Map< string, Map >(); @@ -93,6 +94,42 @@ class RateProviderTaproot extends RateProviderBase { super(currencies, feeProvider, minSwapSizeMultipliers); } + public getSubmarinePairs = ( + referral?: Referral | null, + ): typeof this.submarinePairs => { + if (referral === null || referral === undefined) { + return this.submarinePairs; + } + + return this.deepCloneWithPremium( + this.submarinePairs, + referral.submarinePremium, + ); + }; + + public getReversePairs = ( + referral?: Referral | null, + ): typeof this.reversePairs => { + if (referral === null || referral === undefined) { + return this.reversePairs; + } + + return this.deepCloneWithPremium( + this.reversePairs, + referral.reversePremium, + ); + }; + + public getChainPairs = ( + referral?: Referral | null, + ): typeof this.chainPairs => { + if (referral === null || referral === undefined) { + return this.chainPairs; + } + + return this.deepCloneWithPremium(this.chainPairs, referral.chainPremium); + }; + public static serializePairs = ( map: Map>, ): Record> => { @@ -345,6 +382,7 @@ class RateProviderTaproot extends RateProviderBase { orderSide, type, PercentageFeeType.Display, + null, ), minerFees, }, @@ -443,6 +481,40 @@ class RateProviderTaproot extends RateProviderBase { return cur.chainClient !== undefined || cur.provider !== undefined; }; + + private deepCloneWithPremium = < + T extends + | SubmarinePairTypeTaproot + | ReversePairTypeTaproot + | ChainPairTypeTaproot, + K extends Map>, + >( + map: K, + premium?: number, + ): K => { + return new Map( + Array.from(map.entries()).map(([key, nested]) => [ + key, + new Map( + Array.from(nested.entries()).map(([key, value]) => { + return [ + key, + { + ...value, + fees: { + ...value.fees, + percentage: FeeProvider.addPremium( + value.fees.percentage, + premium, + ), + }, + }, + ]; + }), + ), + ]), + ) as K; + }; } export default RateProviderTaproot; diff --git a/lib/service/Renegotiator.ts b/lib/service/Renegotiator.ts index 0e4aa18c..280a40a6 100644 --- a/lib/service/Renegotiator.ts +++ b/lib/service/Renegotiator.ts @@ -10,9 +10,11 @@ import { SwapVersion, swapTypeToPrettyString, } from '../consts/Enums'; +import Referral from '../db/models/Referral'; import ChainSwapRepository, { ChainSwapInfo, } from '../db/repositories/ChainSwapRepository'; +import ReferralRepository from '../db/repositories/ReferralRepository'; import { ChainSwapMinerFees } from '../rates/FeeProvider'; import RateProvider from '../rates/RateProvider'; import ErrorsSwap from '../swap/Errors'; @@ -47,7 +49,7 @@ class Renegotiator { const { swap, receivingCurrency } = await this.getSwap(swapId); await this.validateEligibility(swap, receivingCurrency); - return this.calculateNewQuote(swap).serverLockAmount; + return (await this.calculateNewQuote(swap)).serverLockAmount; }; // Do this in the refund signature lock to avoid creating refund signatures @@ -59,7 +61,7 @@ class Renegotiator { await this.validateEligibility(swap, receivingCurrency); const { serverLockAmount, percentageFee } = - this.calculateNewQuote(swap); + await this.calculateNewQuote(swap); if (newQuote !== serverLockAmount) { throw Errors.INVALID_QUOTE(); } @@ -183,7 +185,11 @@ class Renegotiator { }; }; - public getFees = (pairId: string, side: OrderSide) => ({ + public getFees = ( + pairId: string, + side: OrderSide, + referral: Referral | null, + ) => ({ baseFee: this.rateProvider.feeProvider.getSwapBaseFees( pairId, side, @@ -195,11 +201,18 @@ class Renegotiator { side, SwapType.Chain, PercentageFeeType.Calculation, + referral, ), }); - private calculateNewQuote = (swap: ChainSwapInfo) => { - const pair = this.rateProvider.providers[SwapVersion.Taproot].chainPairs + private calculateNewQuote = async (swap: ChainSwapInfo) => { + const referral = + swap.chainSwap.referral === null || swap.chainSwap.referral === undefined + ? null + : await ReferralRepository.getReferralById(swap.chainSwap.referral); + + const pair = this.rateProvider.providers[SwapVersion.Taproot] + .getChainPairs(referral) .get(swap.receivingData.symbol) ?.get(swap.sendingData.symbol); if (pair === undefined) { @@ -218,7 +231,11 @@ class Renegotiator { ); } - const { baseFee, feePercent } = this.getFees(swap.pair, swap.orderSide); + const { baseFee, feePercent } = this.getFees( + swap.pair, + swap.orderSide, + referral, + ); return this.calculateServerLockAmount( pair.rate, diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 2757d790..ee84c1b8 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -42,6 +42,7 @@ import { SwapVersion, } from '../consts/Enums'; import { AnySwap, PairConfig } from '../consts/Types'; +import Referral from '../db/models/Referral'; import ReverseSwap from '../db/models/ReverseSwap'; import Swap from '../db/models/Swap'; import ChainSwapRepository, { @@ -1202,6 +1203,9 @@ class Service { swap.orderSide, SwapType.Submarine, PercentageFeeType.Calculation, + swap.referral !== undefined && swap.referral !== null + ? await ReferralRepository.getReferralById(swap.referral) + : null, ); const baseFee = this.rateProvider.feeProvider.getBaseFee( onchainCurrency, @@ -1217,13 +1221,14 @@ class Service { percentageFee, ); - this.verifyAmount( + await this.verifyAmount( swap.pair, rate, invoiceAmount, swap.orderSide, swap.version, SwapType.Submarine, + swap.referral, ); return { @@ -1304,12 +1309,14 @@ class Service { const { base, quote, + referral, rate: pairRate, - } = this.getPair( + } = await this.getPair( swap.pair, swap.orderSide, swap.version, SwapType.Submarine, + swap.referral, ); if (pairHash !== undefined) { @@ -1359,13 +1366,14 @@ class Service { } const rate = swap.rate || getRate(pairRate, swap.orderSide, false); - this.verifyAmount( + await this.verifyAmount( swap.pair, rate, swap.invoiceAmount, swap.orderSide, swap.version, SwapType.Submarine, + swap.referral, ); const { baseFee, percentageFee } = this.rateProvider.feeProvider.getFees( @@ -1376,6 +1384,7 @@ class Service { swap.invoiceAmount, SwapType.Submarine, BaseFeeType.NormalClaim, + referral, ); const expectedAmount = @@ -1392,6 +1401,7 @@ class Service { swap.orderSide, SwapType.Submarine, PercentageFeeType.Calculation, + referral, ), ); @@ -1620,15 +1630,21 @@ class Service { await this.checkSwapWithPreimageExists(args.preimageHash); const side = this.getOrderSide(args.orderSide); + const referralId = await this.getReferralId( + args.referralId, + args.routingNode, + ); const { base, quote, + referral, rate: pairRate, - } = this.getPair( + } = await this.getPair( args.pairId, side, args.version, SwapType.ReverseSubmarine, + referralId, ); if (args.pairHash !== undefined) { @@ -1696,6 +1712,7 @@ class Service { side, SwapType.ReverseSubmarine, PercentageFeeType.Calculation, + referral, ); const baseFee = this.rateProvider.feeProvider.getBaseFee( sendingCurrency.symbol, @@ -1740,13 +1757,14 @@ class Service { throw Errors.NO_AMOUNT_SPECIFIED(); } - this.verifyAmount( + await this.verifyAmount( args.pairId, rate, holdInvoiceAmount, side, args.version, SwapType.ReverseSubmarine, + referralId, ); await this.balanceCheck.checkBalance(sendingCurrency.symbol, onchainAmount); @@ -1810,10 +1828,6 @@ class Service { throw Errors.ONCHAIN_AMOUNT_TOO_LOW(); } - const referralId = await this.getReferralId( - args.referralId, - args.routingNode, - ); const { id, invoice, @@ -1908,11 +1922,19 @@ class Service { await this.checkSwapWithPreimageExists(args.preimageHash); const side = this.getOrderSide(args.orderSide); + const referralId = await this.getReferralId(args.referralId); const { base, quote, + referral, rate: pairRate, - } = this.getPair(args.pairId, side, SwapVersion.Taproot, SwapType.Chain); + } = await this.getPair( + args.pairId, + side, + SwapVersion.Taproot, + SwapType.Chain, + referralId, + ); if (base === quote) { throw Errors.PAIR_NOT_FOUND(args.pairId); @@ -1985,6 +2007,7 @@ class Service { const { feePercent, baseFee } = this.swapManager.renegotiator.getFees( args.pairId, side, + referral, ); let percentageFee: number; @@ -2025,13 +2048,14 @@ class Service { } if (!isZeroAmount) { - this.verifyAmount( + await this.verifyAmount( args.pairId, rate, args.userLockAmount, side, SwapVersion.Taproot, SwapType.Chain, + referralId, ); if (args.serverLockAmount < 1) { throw Errors.ONCHAIN_AMOUNT_TOO_LOW(); @@ -2043,7 +2067,6 @@ class Service { ); } - const referralId = await this.getReferralId(args.referralId); const res = await this.swapManager.createChainSwap({ referralId, percentageFee, @@ -2158,13 +2181,14 @@ class Service { /** * Verifies that the requested amount is neither above the maximal nor beneath the minimal */ - private verifyAmount = ( + private verifyAmount = async ( pairId: string, rate: number, amount: number, orderSide: OrderSide, version: SwapVersion, type: SwapType, + referralId?: string, ) => { if ( (type === SwapType.Submarine && orderSide === OrderSide.BUY) || @@ -2173,7 +2197,13 @@ class Service { amount = Math.floor(amount * rate); } - const { limits } = this.getPair(pairId, orderSide, version, type); + const { limits } = await this.getPair( + pairId, + orderSide, + version, + type, + referralId, + ); if (limits) { if (Math.floor(amount) > limits.maximal) @@ -2241,15 +2271,19 @@ class Service { return Math.floor(((onchainAmount - baseFee) * rate) / (1 + percentageFee)); }; - private getPair = ( + private getPair = async ( pairId: string, orderSide: OrderSide, version: SwapVersion, type: SwapType, - ): { base: string; quote: string } & SomePair => { + referralId?: string, + ): Promise< + { base: string; quote: string; referral: Referral | null } & SomePair + > => { const { base, quote } = splitPairId(pairId); let pair: SomePair | undefined; + let referral: Referral | null = null; switch (version) { case SwapVersion.Taproot: { @@ -2260,18 +2294,23 @@ class Service { orderSide, ); + referral = + referralId !== undefined && referralId !== null + ? await ReferralRepository.getReferralById(referralId) + : null; + let pairMap: Map> | undefined; switch (type) { case SwapType.Submarine: - pairMap = provider.submarinePairs; + pairMap = provider.getSubmarinePairs(referral); break; case SwapType.ReverseSubmarine: - pairMap = provider.reversePairs; + pairMap = provider.getReversePairs(referral); break; case SwapType.Chain: - pairMap = provider.chainPairs; + pairMap = provider.getChainPairs(referral); break; } @@ -2292,6 +2331,7 @@ class Service { return { base, quote, + referral, ...pair, }; }; diff --git a/lib/swap/PaymentHandler.ts b/lib/swap/PaymentHandler.ts index cc8fc932..1d39b778 100644 --- a/lib/swap/PaymentHandler.ts +++ b/lib/swap/PaymentHandler.ts @@ -326,7 +326,7 @@ class PaymentHandler { response: PaymentResponse, ): Promise => { this.logger.verbose( - `Paid invoice of Swap ${swap.id}: ${getHexString(response.preimage)}`, + `Paid invoice of Swap ${swap.id} (${swap.preimageHash}): ${getHexString(response.preimage)}`, ); this.emit( diff --git a/test/integration/db/models/Referral.spec.ts b/test/integration/db/models/Referral.spec.ts new file mode 100644 index 00000000..ee474842 --- /dev/null +++ b/test/integration/db/models/Referral.spec.ts @@ -0,0 +1,43 @@ +import Logger from '../../../../lib/Logger'; +import { createApiCredential } from '../../../../lib/Utils'; +import { SwapType } from '../../../../lib/consts/Enums'; +import Database from '../../../../lib/db/Database'; +import ReferralRepository, { + ReferralType, +} from '../../../../lib/db/repositories/ReferralRepository'; + +describe('Referral', () => { + const db = new Database(Logger.disabledLogger, Database.memoryDatabase); + + const referralValues: ReferralType = { + id: 'test', + apiKey: createApiCredential(), + apiSecret: createApiCredential(), + feeShare: 0, + submarinePremium: 10, + reversePremium: 20, + chainPremium: 30, + }; + + beforeAll(async () => { + await db.init(); + + await ReferralRepository.addReferral(referralValues); + }); + + afterAll(async () => { + await db.close(); + }); + + test.each` + type | value + ${SwapType.Submarine} | ${referralValues.submarinePremium} + ${SwapType.ReverseSubmarine} | ${referralValues.reversePremium} + ${SwapType.Chain} | ${referralValues.chainPremium} + `('should get premium for type $type', async ({ type, value }) => { + const ref = await ReferralRepository.getReferralById(referralValues.id); + expect(ref).not.toBeNull(); + + expect(ref!.premiumForType(type)).toEqual(value); + }); +}); diff --git a/test/integration/service/Renegotiator.spec.ts b/test/integration/service/Renegotiator.spec.ts index 4a45c1c4..2bc6eab3 100644 --- a/test/integration/service/Renegotiator.spec.ts +++ b/test/integration/service/Renegotiator.spec.ts @@ -11,6 +11,7 @@ import { SwapVersion, } from '../../../lib/consts/Enums'; import ChainSwapRepository from '../../../lib/db/repositories/ChainSwapRepository'; +import ReferralRepository from '../../../lib/db/repositories/ReferralRepository'; import RateProvider from '../../../lib/rates/RateProvider'; import BalanceCheck from '../../../lib/service/BalanceCheck'; import Errors from '../../../lib/service/Errors'; @@ -73,53 +74,55 @@ describe('Renegotiator', () => { const rateProvider = { providers: { [SwapVersion.Taproot]: { - chainPairs: new Map([ - [ - 'BTC', - new Map([ - [ - 'BTC', - { - rate: 1, - limits: { - minimal: 1_000, - maximal: 100_000, + getChainPairs: jest.fn().mockReturnValue( + new Map([ + [ + 'BTC', + new Map([ + [ + 'BTC', + { + rate: 1, + limits: { + minimal: 1_000, + maximal: 100_000, + }, }, - }, - ], - ]), - ], - [ - 'RBTC', - new Map([ - [ - 'BTC', - { - rate: 1, - limits: { - minimal: 1_000, - maximal: 100_000, + ], + ]), + ], + [ + 'RBTC', + new Map([ + [ + 'BTC', + { + rate: 1, + limits: { + minimal: 1_000, + maximal: 100_000, + }, }, - }, - ], - ]), - ], - [ - 'TBTC', - new Map([ - [ - 'BTC', - { - rate: 1, - limits: { - minimal: 1_000, - maximal: 100_000, + ], + ]), + ], + [ + 'TBTC', + new Map([ + [ + 'BTC', + { + rate: 1, + limits: { + minimal: 1_000, + maximal: 100_000, + }, }, - }, - ], - ]), - ], - ]), + ], + ]), + ], + ]), + ), }, }, feeProvider: { @@ -183,6 +186,7 @@ describe('Renegotiator', () => { const swapId = 'someId'; ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'BTC', amount: 100_000, @@ -203,6 +207,7 @@ describe('Renegotiator', () => { const swapId = 'someId'; ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'BTC', amount: 100_000, @@ -224,6 +229,7 @@ describe('Renegotiator', () => { const swapId = 'someId'; ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'BTC', amount: 100_000, @@ -270,6 +276,7 @@ describe('Renegotiator', () => { } ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { transactionId, symbol: 'BTC', @@ -321,6 +328,7 @@ describe('Renegotiator', () => { describe('EVM chain', () => { test('should throw when there is no receipt for transaction', async () => { ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'RBTC', amount: 100_000, @@ -353,6 +361,7 @@ describe('Renegotiator', () => { await transaction.wait(1); ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'RBTC', amount: 100_000, @@ -396,6 +405,7 @@ describe('Renegotiator', () => { await transaction.wait(1); ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'RBTC', amount: 100_000, @@ -462,6 +472,7 @@ describe('Renegotiator', () => { await transaction.wait(1); ChainSwapRepository.getChainSwap = jest.fn().mockResolvedValue({ + chainSwap: {}, receivingData: { symbol: 'TBTC', amount: 100_000, @@ -535,7 +546,7 @@ describe('Renegotiator', () => { const pair = 'BTC/BTC'; const side = OrderSide.BUY; - expect(negotiator.getFees(pair, side)).toEqual({ + expect(negotiator.getFees(pair, side, null)).toEqual({ baseFee: 123, feePercent: 0.05, }); @@ -554,14 +565,39 @@ describe('Renegotiator', () => { side, SwapType.Chain, PercentageFeeType.Calculation, + null, ); }); describe('calculateNewQuote', () => { test('should calculate new quotes', async () => { - expect( + await expect( + negotiator['calculateNewQuote']({ + pair: 'BTC/BTC', + chainSwap: {}, + receivingData: { + symbol: 'BTC', + amount: 10_000, + }, + sendingData: { + symbol: 'BTC', + }, + } as any), + ).resolves.toEqual({ percentageFee: 500, serverLockAmount: 9377 }); + }); + + test('should calculate new quotes with a referral premium', async () => { + const referral = { some: 'data' }; + ReferralRepository.getReferralById = jest + .fn() + .mockResolvedValue(referral); + + await expect( negotiator['calculateNewQuote']({ pair: 'BTC/BTC', + chainSwap: { + referral: 'id', + }, receivingData: { symbol: 'BTC', amount: 10_000, @@ -570,13 +606,24 @@ describe('Renegotiator', () => { symbol: 'BTC', }, } as any), - ).toEqual({ percentageFee: 500, serverLockAmount: 9377 }); + ).resolves.toEqual({ percentageFee: 500, serverLockAmount: 9377 }); + + expect(ReferralRepository.getReferralById).toHaveBeenCalledTimes(1); + expect(ReferralRepository.getReferralById).toHaveBeenCalledWith('id'); + + expect( + rateProvider.providers[SwapVersion.Taproot].getChainPairs, + ).toHaveBeenCalledTimes(1); + expect( + rateProvider.providers[SwapVersion.Taproot].getChainPairs, + ).toHaveBeenCalledWith(referral); }); - test('should throw when pair cannot be found', () => { - expect(() => + test('should throw when pair cannot be found', async () => { + await expect( negotiator['calculateNewQuote']({ pair: 'not/found', + chainSwap: {}, receivingData: { symbol: 'not', }, @@ -584,13 +631,14 @@ describe('Renegotiator', () => { symbol: 'found', }, } as any), - ).toThrow(Errors.PAIR_NOT_FOUND('not/found').message); + ).rejects.toEqual(Errors.PAIR_NOT_FOUND('not/found')); }); - test('should throw when limit is more than maxima', () => { - expect(() => + test('should throw when limit is more than maxima', async () => { + await expect( negotiator['calculateNewQuote']({ pair: 'BTC/BTC', + chainSwap: {}, receivingData: { symbol: 'BTC', amount: 100_001, @@ -599,13 +647,14 @@ describe('Renegotiator', () => { symbol: 'BTC', }, } as any), - ).toThrow(Errors.EXCEED_MAXIMAL_AMOUNT(100_001, 100_000).message); + ).rejects.toEqual(Errors.EXCEED_MAXIMAL_AMOUNT(100_001, 100_000)); }); - test('should throw when limit is less than minima', () => { - expect(() => + test('should throw when limit is less than minima', async () => { + await expect( negotiator['calculateNewQuote']({ pair: 'BTC/BTC', + chainSwap: {}, receivingData: { symbol: 'BTC', amount: 999, @@ -614,7 +663,7 @@ describe('Renegotiator', () => { symbol: 'BTC', }, } as any), - ).toThrow(Errors.BENEATH_MINIMAL_AMOUNT(999, 1_000).message); + ).rejects.toEqual(Errors.BENEATH_MINIMAL_AMOUNT(999, 1_000)); }); }); diff --git a/test/unit/api/Utils.ts b/test/unit/api/Utils.ts index 4c571bfc..58e62593 100644 --- a/test/unit/api/Utils.ts +++ b/test/unit/api/Utils.ts @@ -10,7 +10,8 @@ export const mockRequest = ( query, params, ip: '127.0.0.1', - }) as Request; + header: jest.fn().mockReturnValue(undefined), + }) as unknown as Request; export const mockResponse = () => { const res = {} as any as Response; diff --git a/test/unit/api/v2/routers/ReferralRouter.spec.ts b/test/unit/api/v2/routers/ReferralRouter.spec.ts index ae31436e..7d646afb 100644 --- a/test/unit/api/v2/routers/ReferralRouter.spec.ts +++ b/test/unit/api/v2/routers/ReferralRouter.spec.ts @@ -131,16 +131,12 @@ describe('ReferralRouter', () => { some: 'data', }; + const req = mockRequest(); const res = mockResponse(); - const referral = await referralRouter['checkAuthentication']( - mockRequest(), - res, - ); + const referral = await referralRouter['checkAuthentication'](req, res); expect(referral).toEqual(mockValidateAuthResult); - expect(Bouncer.validateRequestAuthentication).toHaveBeenCalledWith( - mockRequest(), - ); + expect(Bouncer.validateRequestAuthentication).toHaveBeenCalledWith(req); expect(res.status).not.toHaveBeenCalled(); expect(res.json).not.toHaveBeenCalled(); @@ -149,12 +145,11 @@ describe('ReferralRouter', () => { test('should write error response with invalid authentication', async () => { mockValidateAuthResult = null; + const req = mockRequest(); const res = mockResponse(); - await referralRouter['checkAuthentication'](mockRequest(), res); + await referralRouter['checkAuthentication'](req, res); - expect(Bouncer.validateRequestAuthentication).toHaveBeenCalledWith( - mockRequest(), - ); + expect(Bouncer.validateRequestAuthentication).toHaveBeenCalledWith(req); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'unauthorized' }); diff --git a/test/unit/api/v2/routers/SwapRouter.spec.ts b/test/unit/api/v2/routers/SwapRouter.spec.ts index a65f53c4..26dd40a7 100644 --- a/test/unit/api/v2/routers/SwapRouter.spec.ts +++ b/test/unit/api/v2/routers/SwapRouter.spec.ts @@ -8,6 +8,7 @@ import SwapRouter from '../../../../../lib/api/v2/routers/SwapRouter'; import { OrderSide, SwapVersion } from '../../../../../lib/consts/Enums'; import ChainSwapRepository from '../../../../../lib/db/repositories/ChainSwapRepository'; import MarkedSwapRepository from '../../../../../lib/db/repositories/MarkedSwapRepository'; +import ReferralRepository from '../../../../../lib/db/repositories/ReferralRepository'; import SwapRepository from '../../../../../lib/db/repositories/SwapRepository'; import RateProviderTaproot from '../../../../../lib/rates/providers/RateProviderTaproot'; import CountryCodes from '../../../../../lib/service/CountryCodes'; @@ -47,21 +48,36 @@ describe('SwapRouter', () => { rateProvider: { providers: { [SwapVersion.Taproot]: { - submarinePairs: new Map>([ - [ - 'L-BTC', - new Map([['BTC', { some: 'submarine data' }]]), - ], - ]), - reversePairs: new Map>([ - [ - 'BTC', - new Map([['L-BTC', { some: 'reverse data' }]]), - ], - ]), - chainPairs: new Map>([ - ['BTC', new Map([['L-BTC', { some: 'chain data' }]])], - ]), + getSubmarinePairs: jest + .fn() + .mockReturnValue( + new Map>([ + [ + 'L-BTC', + new Map([['BTC', { some: 'submarine data' }]]), + ], + ]), + ), + getReversePairs: jest + .fn() + .mockReturnValue( + new Map>([ + [ + 'BTC', + new Map([['L-BTC', { some: 'reverse data' }]]), + ], + ]), + ), + getChainPairs: jest + .fn() + .mockReturnValue( + new Map>([ + [ + 'BTC', + new Map([['L-BTC', { some: 'chain data' }]]), + ], + ]), + ), }, }, }, @@ -325,14 +341,14 @@ describe('SwapRouter', () => { }); }); - test('should get submarine pairs', () => { + test('should get submarine pairs', async () => { const res = mockResponse(); - swapRouter['getSubmarine'](mockRequest(), res); + await swapRouter['getSubmarine'](mockRequest(), res); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith( RateProviderTaproot.serializePairs( - service.rateProvider.providers[SwapVersion.Taproot].submarinePairs, + service.rateProvider.providers[SwapVersion.Taproot].getSubmarinePairs(), ), ); }); @@ -915,14 +931,14 @@ describe('SwapRouter', () => { expect(res.json).toHaveBeenCalledWith({}); }); - test('should get reverse pairs', () => { + test('should get reverse pairs', async () => { const res = mockResponse(); - swapRouter['getReverse'](mockRequest(), res); + await swapRouter['getReverse'](mockRequest(), res); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith( RateProviderTaproot.serializePairs( - service.rateProvider.providers[SwapVersion.Taproot].reversePairs, + service.rateProvider.providers[SwapVersion.Taproot].getReversePairs(), ), ); }); @@ -1405,14 +1421,14 @@ describe('SwapRouter', () => { ); }); - test('should get chain swap pairs', () => { + test('should get chain swap pairs', async () => { const res = mockResponse(); - swapRouter['getChain'](mockRequest(), res); + await swapRouter['getChain'](mockRequest(), res); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith( RateProviderTaproot.serializePairs( - service.rateProvider.providers[SwapVersion.Taproot].chainPairs, + service.rateProvider.providers[SwapVersion.Taproot].getChainPairs(), ), ); }); @@ -1839,4 +1855,34 @@ describe('SwapRouter', () => { }); }); }); + + describe('getReferralFromHeader', () => { + test('should query referral from header', async () => { + const referral = { + ref: 'id', + }; + ReferralRepository.getReferralById = jest + .fn() + .mockResolvedValue(referral); + + const req = mockRequest(); + req.header = jest.fn().mockReturnValue('id'); + await expect(swapRouter['getReferralFromHeader'](req)).resolves.toEqual( + referral, + ); + + expect(req.header).toHaveBeenCalledWith('referral'); + expect(ReferralRepository.getReferralById).toHaveBeenCalledWith('id'); + }); + + test('should not query referral when header is not set', async () => { + ReferralRepository.getReferralById = jest.fn().mockResolvedValue({}); + + await expect( + swapRouter['getReferralFromHeader'](mockRequest()), + ).resolves.toEqual(null); + + expect(ReferralRepository.getReferralById).not.toHaveBeenCalled(); + }); + }); }); diff --git a/test/unit/rates/FeeProvider.spec.ts b/test/unit/rates/FeeProvider.spec.ts index f4a37111..080c1bfa 100644 --- a/test/unit/rates/FeeProvider.spec.ts +++ b/test/unit/rates/FeeProvider.spec.ts @@ -6,6 +6,7 @@ import { SwapType, SwapVersion, } from '../../../lib/consts/Enums'; +import Referral from '../../../lib/db/models/Referral'; import FeeProvider from '../../../lib/rates/FeeProvider'; import DataAggregator from '../../../lib/rates/data/DataAggregator'; import WalletLiquid from '../../../lib/wallet/WalletLiquid'; @@ -64,6 +65,17 @@ describe('FeeProvider', () => { getFeeEstimation, ); + test.each` + fee | premium | expected + ${0.1} | ${null} | ${0.1} + ${0.1} | ${undefined} | ${0.1} + ${0.1} | ${10} | ${0.2} + ${0.2} | ${-25} | ${-0.05} + ${0.101} | ${10} | ${0.2} + `('should add premium', ({ fee, premium, expected }) => { + expect(FeeProvider.addPremium(fee, premium)).toEqual(expected); + }); + test('should init', () => { feeProvider.init([ { @@ -130,102 +142,167 @@ describe('FeeProvider', () => { }); }); - test('should get percentage fees of normal swaps', () => { - expect( - feeProvider.getPercentageFee( - 'LTC/BTC', - OrderSide.BUY, - SwapType.Submarine, - ), - ).toEqual(-0.01); - expect( - feeProvider.getPercentageFee( - 'LTC/BTC', - OrderSide.BUY, - SwapType.Submarine, - PercentageFeeType.Display, - ), - ).toEqual(-1); + describe('getPercentageFee', () => { + test('should get percentage fees of normal swaps', () => { + expect( + feeProvider.getPercentageFee( + 'LTC/BTC', + OrderSide.BUY, + SwapType.Submarine, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(-0.01); + expect( + feeProvider.getPercentageFee( + 'LTC/BTC', + OrderSide.BUY, + SwapType.Submarine, + PercentageFeeType.Display, + null, + ), + ).toEqual(-1); - expect( - feeProvider.getPercentageFee( - 'BTC/BTC', - OrderSide.BUY, - SwapType.Submarine, - PercentageFeeType.Calculation, - ), - ).toEqual(-0.01); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.BUY, + SwapType.Submarine, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(-0.01); + + // Should set undefined fees to 1% + expect( + feeProvider.getPercentageFee( + 'LTC/LTC', + OrderSide.BUY, + SwapType.Submarine, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.01); + }); - // Should set undefined fees to 1% - expect( - feeProvider.getPercentageFee( - 'LTC/LTC', - OrderSide.BUY, - SwapType.Submarine, - PercentageFeeType.Calculation, - ), - ).toEqual(0.01); - }); + test('should get percentage fees of reverse swaps', () => { + expect( + feeProvider.getPercentageFee( + 'LTC/BTC', + OrderSide.BUY, + SwapType.ReverseSubmarine, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.02); + expect( + feeProvider.getPercentageFee( + 'LTC/BTC', + OrderSide.BUY, + SwapType.ReverseSubmarine, + PercentageFeeType.Display, + null, + ), + ).toEqual(2); - test('should get percentage fees of reverse swaps', () => { - expect( - feeProvider.getPercentageFee( - 'LTC/BTC', - OrderSide.BUY, - SwapType.ReverseSubmarine, - ), - ).toEqual(0.02); - expect( - feeProvider.getPercentageFee( - 'LTC/BTC', - OrderSide.BUY, - SwapType.ReverseSubmarine, - PercentageFeeType.Display, - ), - ).toEqual(2); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.BUY, + SwapType.ReverseSubmarine, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0); + + // Should set undefined fees to 1% + expect( + feeProvider.getPercentageFee( + 'LTC/LTC', + OrderSide.BUY, + SwapType.ReverseSubmarine, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.01); + }); - expect( - feeProvider.getPercentageFee( - 'BTC/BTC', - OrderSide.BUY, - SwapType.ReverseSubmarine, - ), - ).toEqual(0); + test('should get percentage fees of chain swaps', () => { + expect( + feeProvider.getPercentageFee( + 'LTC/BTC', + OrderSide.BUY, + SwapType.Chain, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.02); - // Should set undefined fees to 1% - expect( - feeProvider.getPercentageFee( - 'LTC/LTC', - OrderSide.BUY, - SwapType.ReverseSubmarine, - ), - ).toEqual(0.01); - }); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.BUY, + SwapType.Chain, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.01); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.SELL, + SwapType.Chain, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.02); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.SELL, + SwapType.Chain, + PercentageFeeType.Display, + null, + ), + ).toEqual(2); + + // Should set undefined fees to 1% + expect( + feeProvider.getPercentageFee( + 'LTC/LTC', + OrderSide.BUY, + SwapType.Chain, + PercentageFeeType.Calculation, + null, + ), + ).toEqual(0.01); + }); - test('should get percentage fees of chain swaps', () => { - expect( - feeProvider.getPercentageFee('LTC/BTC', OrderSide.BUY, SwapType.Chain), - ).toEqual(0.02); + test('should get percentage fees with premium', () => { + const referral = { + premiumForType: jest.fn().mockReturnValue(20), + } as unknown as Referral; - expect( - feeProvider.getPercentageFee('BTC/BTC', OrderSide.BUY, SwapType.Chain), - ).toEqual(0.01); - expect( - feeProvider.getPercentageFee('BTC/BTC', OrderSide.SELL, SwapType.Chain), - ).toEqual(0.02); - expect( - feeProvider.getPercentageFee( - 'BTC/BTC', - OrderSide.SELL, - SwapType.Chain, - PercentageFeeType.Display, - ), - ).toEqual(2); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.BUY, + SwapType.Chain, + PercentageFeeType.Calculation, + referral, + ), + ).toEqual(0.012); - // Should set undefined fees to 1% - expect( - feeProvider.getPercentageFee('LTC/LTC', OrderSide.BUY, SwapType.Chain), - ).toEqual(0.01); + expect( + feeProvider.getPercentageFee( + 'BTC/BTC', + OrderSide.BUY, + SwapType.Chain, + PercentageFeeType.Display, + referral, + ), + ).toEqual(1.2); + }); }); test('should update miner fees', async () => { @@ -339,6 +416,7 @@ describe('FeeProvider', () => { amount, SwapType.Submarine, BaseFeeType.NormalClaim, + null, ), ).toEqual({ baseFee: 6120, @@ -354,6 +432,7 @@ describe('FeeProvider', () => { amount, SwapType.Submarine, BaseFeeType.NormalClaim, + null, ), ).toEqual({ baseFee: 5436, @@ -369,6 +448,7 @@ describe('FeeProvider', () => { amount, SwapType.ReverseSubmarine, BaseFeeType.ReverseLockup, + null, ), ).toEqual({ baseFee: 459, @@ -384,11 +464,32 @@ describe('FeeProvider', () => { amount, SwapType.ReverseSubmarine, BaseFeeType.ReverseLockup, + null, ), ).toEqual({ baseFee: 462, percentageFee: 4000000, }); + + const referral = { + premiumForType: jest.fn().mockReturnValue(20), + } as unknown as Referral; + + expect( + feeProvider.getFees( + 'LTC/BTC', + SwapVersion.Taproot, + 2, + OrderSide.BUY, + amount, + SwapType.ReverseSubmarine, + BaseFeeType.ReverseLockup, + referral, + ), + ).toEqual({ + baseFee: 462, + percentageFee: 4400000, + }); }); test.each` diff --git a/test/unit/rates/providers/RateProviderLegacy.spec.ts b/test/unit/rates/providers/RateProviderLegacy.spec.ts index 4cb43c46..461d18a2 100644 --- a/test/unit/rates/providers/RateProviderLegacy.spec.ts +++ b/test/unit/rates/providers/RateProviderLegacy.spec.ts @@ -124,11 +124,15 @@ describe('RateProviderLegacy', () => { 'L-BTC/BTC', OrderSide.BUY, SwapType.ReverseSubmarine, + PercentageFeeType.Calculation, + null, ), percentageSwapIn: mockedFeeProvider.getPercentageFee( 'L-BTC/BTC', OrderSide.BUY, SwapType.Submarine, + PercentageFeeType.Calculation, + null, ), minerFees: { baseAsset: { @@ -154,12 +158,14 @@ describe('RateProviderLegacy', () => { OrderSide.BUY, SwapType.Submarine, PercentageFeeType.Display, + null, ); expect(mockedFeeProvider.getPercentageFee).toHaveBeenCalledWith( 'L-BTC/BTC', OrderSide.BUY, SwapType.ReverseSubmarine, PercentageFeeType.Display, + null, ); }); @@ -176,11 +182,15 @@ describe('RateProviderLegacy', () => { 'L-BTC/BTC', OrderSide.BUY, SwapType.ReverseSubmarine, + PercentageFeeType.Calculation, + null, ), percentageSwapIn: mockedFeeProvider.getPercentageFee( 'L-BTC/BTC', OrderSide.BUY, SwapType.Submarine, + PercentageFeeType.Calculation, + null, ), minerFees: { baseAsset: { @@ -216,11 +226,15 @@ describe('RateProviderLegacy', () => { 'L-BTC/BTC', OrderSide.BUY, SwapType.ReverseSubmarine, + PercentageFeeType.Calculation, + null, ), percentageSwapIn: mockedFeeProvider.getPercentageFee( 'L-BTC/BTC', OrderSide.BUY, SwapType.Submarine, + PercentageFeeType.Calculation, + null, ), minerFees: { baseAsset: { diff --git a/test/unit/rates/providers/RateProviderTaproot.spec.ts b/test/unit/rates/providers/RateProviderTaproot.spec.ts index 6d6a13f3..62015997 100644 --- a/test/unit/rates/providers/RateProviderTaproot.spec.ts +++ b/test/unit/rates/providers/RateProviderTaproot.spec.ts @@ -40,6 +40,12 @@ jest.mock('../../../../lib/rates/FeeProvider', () => { const mockedFeeProvider = (>(FeeProvider))(); +FeeProvider.addPremium = jest + .fn() + .mockImplementation((fee, premium) => + parseFloat((fee + premium / 100).toFixed(2)), + ); + describe('RateProviderTaproot', () => { const pairConfigs = [ { @@ -82,14 +88,85 @@ describe('RateProviderTaproot', () => { ); beforeEach(() => { - provider.reversePairs.clear(); - provider.submarinePairs.clear(); + provider['submarinePairs'].clear(); + provider['reversePairs'].clear(); + provider['chainPairs'].clear(); jest.clearAllMocks(); }); + describe('pairs with premium', () => { + beforeEach(() => { + provider.setHardcodedPair( + { + base: 'L-BTC', + quote: 'BTC', + rate: 1, + } as any, + [SwapType.Submarine, SwapType.ReverseSubmarine, SwapType.Chain], + ); + }); + + test('should calculate submarine pairs with premium', () => { + expect( + provider.getSubmarinePairs().get('L-BTC')!.get('BTC')!.fees.percentage, + ).toEqual(0.1); + + expect( + provider + .getSubmarinePairs({ + submarinePremium: 10, + } as any) + .get('L-BTC')! + .get('BTC')!.fees.percentage, + ).toEqual(0.2); + + expect( + provider.getSubmarinePairs().get('L-BTC')!.get('BTC')!.fees.percentage, + ).toEqual(0.1); + }); + + test('should calculate reverse pairs with premium', () => { + expect( + provider.getReversePairs().get('BTC')!.get('L-BTC')!.fees.percentage, + ).toEqual(0.5); + + expect( + provider + .getReversePairs({ + reversePremium: 15, + } as any) + .get('BTC')! + .get('L-BTC')!.fees.percentage, + ).toEqual(0.65); + + expect( + provider.getReversePairs().get('BTC')!.get('L-BTC')!.fees.percentage, + ).toEqual(0.5); + }); + + test('should calculate chain pairs with premium', () => { + expect( + provider.getChainPairs().get('BTC')!.get('L-BTC')!.fees.percentage, + ).toEqual(0.25); + + expect( + provider + .getChainPairs({ + chainPremium: -10, + } as any) + .get('BTC')! + .get('L-BTC')!.fees.percentage, + ).toEqual(0.15); + + expect( + provider.getChainPairs().get('BTC')!.get('L-BTC')!.fees.percentage, + ).toEqual(0.25); + }); + }); + test('should serialize pairs', () => { - provider.submarinePairs.set( + provider['submarinePairs'].set( 'BTC', new Map([ [ @@ -100,9 +177,11 @@ describe('RateProviderTaproot', () => { ], ]), ); - provider.submarinePairs.set('deleted', new Map()); + provider['submarinePairs'].set('deleted', new Map()); - const obj = RateProviderTaproot.serializePairs(provider.submarinePairs); + const obj = RateProviderTaproot.serializePairs( + provider.getSubmarinePairs(), + ); expect(obj).toEqual({ BTC: { 'L-BTC': { @@ -124,11 +203,13 @@ describe('RateProviderTaproot', () => { [SwapType.Submarine, SwapType.ReverseSubmarine], ); - expect(provider.submarinePairs.get('L-BTC')!.get('BTC')!.rate).toEqual( + expect(provider.getSubmarinePairs().get('L-BTC')!.get('BTC')!.rate).toEqual( 1 / rate, ); - expect(provider.reversePairs.get('BTC')!.get('L-BTC')!.rate).toEqual(rate); - expect(provider.chainPairs.get('BTC')).toBeUndefined(); + expect(provider.getReversePairs().get('BTC')!.get('L-BTC')!.rate).toEqual( + rate, + ); + expect(provider.getChainPairs().get('BTC')).toBeUndefined(); }); test('should update pair', () => { @@ -139,10 +220,12 @@ describe('RateProviderTaproot', () => { SwapType.ReverseSubmarine, ]); - expect(provider.submarinePairs.get('L-BTC')!.get('BTC')!.rate).toEqual( + expect(provider.getSubmarinePairs().get('L-BTC')!.get('BTC')!.rate).toEqual( 1 / rate, ); - expect(provider.reversePairs.get('BTC')!.get('L-BTC')!.rate).toEqual(rate); + expect(provider.getReversePairs().get('BTC')!.get('L-BTC')!.rate).toEqual( + rate, + ); }); test('should update hardcoded pairs', () => { @@ -157,26 +240,26 @@ describe('RateProviderTaproot', () => { const wrongHash = 'wrongHash'; - provider.submarinePairs.get('L-BTC')!.get('BTC')!.hash = wrongHash; - provider.reversePairs.get('BTC')!.get('L-BTC')!.hash = wrongHash; + provider.getSubmarinePairs().get('L-BTC')!.get('BTC')!.hash = wrongHash; + provider.getReversePairs().get('BTC')!.get('L-BTC')!.hash = wrongHash; provider.updateHardcodedPair('L-BTC/BTC', [ SwapType.Submarine, SwapType.ReverseSubmarine, ]); - expect(provider.submarinePairs.get('L-BTC')!.get('BTC')!.hash).not.toEqual( - wrongHash, - ); - expect(provider.reversePairs.get('BTC')!.get('L-BTC')!.hash).not.toEqual( - wrongHash, - ); + expect( + provider.getSubmarinePairs().get('L-BTC')!.get('BTC')!.hash, + ).not.toEqual(wrongHash); + expect( + provider.getReversePairs().get('BTC')!.get('L-BTC')!.hash, + ).not.toEqual(wrongHash); }); test('should validate pair hash', () => { const hash = 'hash'; - provider.submarinePairs.set( + provider['submarinePairs'].set( 'BTC', new Map([ [ @@ -197,7 +280,7 @@ describe('RateProviderTaproot', () => { }); test('should throw when validating pair hash when hash is invalid', () => { - provider.submarinePairs.set( + provider['submarinePairs'].set( 'BTC', new Map([ [ @@ -230,7 +313,7 @@ describe('RateProviderTaproot', () => { ), ).toThrow(Errors.PAIR_NOT_FOUND(pair).message); - provider.submarinePairs.set('found', new Map()); + provider['submarinePairs'].set('found', new Map()); expect(() => provider.validatePairHash( @@ -356,9 +439,9 @@ describe('RateProviderTaproot', () => { } as any, ); - expect(provider.reversePairs.size).toEqual(1); - expect(provider.reversePairs.get('BTC')!.size).toEqual(1); - expect(provider.reversePairs.get('BTC')!.get('L-BTC')).toEqual({ + expect(provider.getReversePairs().size).toEqual(1); + expect(provider.getReversePairs().get('BTC')!.size).toEqual(1); + expect(provider.getReversePairs().get('BTC')!.get('L-BTC')).toEqual({ hash: expect.anything(), rate: 1, limits: { @@ -373,7 +456,7 @@ describe('RateProviderTaproot', () => { }, }); - expect(provider.submarinePairs.size).toEqual(0); + expect(provider.getSubmarinePairs().size).toEqual(0); }); test('should set pair and get miner fees when not provided', () => { @@ -385,9 +468,9 @@ describe('RateProviderTaproot', () => { 1, ); - expect(provider.reversePairs.size).toEqual(1); - expect(provider.reversePairs.get('BTC')!.size).toEqual(1); - expect(provider.reversePairs.get('BTC')!.get('L-BTC')).toEqual({ + expect(provider.getReversePairs().size).toEqual(1); + expect(provider.getReversePairs().get('BTC')!.size).toEqual(1); + expect(provider.getReversePairs().get('BTC')!.get('L-BTC')).toEqual({ hash: expect.anything(), rate: 1, limits: { @@ -403,7 +486,7 @@ describe('RateProviderTaproot', () => { }, }); - expect(provider.submarinePairs.size).toEqual(0); + expect(provider.getSubmarinePairs().size).toEqual(0); }); test('should not set pair no rate is provided and none was set prior', () => { @@ -414,9 +497,9 @@ describe('RateProviderTaproot', () => { SwapType.ReverseSubmarine, ); - expect(provider.reversePairs.size).toEqual(1); - expect(provider.reversePairs.get('BTC')!.size).toEqual(0); - expect(provider.submarinePairs.size).toEqual(0); + expect(provider.getReversePairs().size).toEqual(1); + expect(provider.getReversePairs().get('BTC')!.size).toEqual(0); + expect(provider.getSubmarinePairs().size).toEqual(0); }); test('should not set pair when combination is not possible', () => { @@ -426,9 +509,9 @@ describe('RateProviderTaproot', () => { OrderSide.BUY, SwapType.Submarine, ); - expect(provider.reversePairs.size).toEqual(0); - expect(provider.submarinePairs.size).toEqual(1); - expect(provider.submarinePairs.get('BTC')!.size).toEqual(0); + expect(provider.getReversePairs().size).toEqual(0); + expect(provider.getSubmarinePairs().size).toEqual(1); + expect(provider.getSubmarinePairs().get('BTC')!.size).toEqual(0); }); test('should get limits with maximal 0-conf amount for reverse swaps', () => { diff --git a/test/unit/service/Service.spec.ts b/test/unit/service/Service.spec.ts index deb9ab2e..2d6ad526 100644 --- a/test/unit/service/Service.spec.ts +++ b/test/unit/service/Service.spec.ts @@ -504,9 +504,9 @@ jest.mock('../../../lib/rates/RateProvider', () => { }), }, [SwapVersion.Taproot]: { - chainPairs: pairsTaprootChain, - reversePairs: pairsTaprootReverse, - submarinePairs: pairsTaprootSubmarine, + getChainPairs: jest.fn().mockReturnValue(pairsTaprootChain), + getReversePairs: jest.fn().mockReturnValue(pairsTaprootReverse), + getSubmarinePairs: jest.fn().mockReturnValue(pairsTaprootSubmarine), validatePairHash: jest.fn().mockImplementation((hash) => { if (['', 'wrongHash'].includes(hash)) { throw Errors.INVALID_PAIR_HASH(); @@ -1950,6 +1950,7 @@ describe('Service', () => { invoiceAmount, SwapType.Submarine, BaseFeeType.NormalClaim, + null, ); expect(mockAcceptZeroConf).toHaveBeenCalledTimes(1); @@ -2290,6 +2291,7 @@ describe('Service', () => { OrderSide.BUY, SwapType.ReverseSubmarine, PercentageFeeType.Calculation, + null, ); expect(mockGetBaseFee).toHaveBeenCalledTimes(1); @@ -2966,8 +2968,8 @@ describe('Service', () => { ${21_000} | ${OrderSide.SELL} | ${Errors.EXCEED_MAXIMAL_AMOUNT(21_000, 10)} `( 'should throw when submarine amount is out of bounds', - ({ amount, side, error }) => { - expect(() => + async ({ amount, side, error }) => { + await expect( verifyAmount( pair, rate, @@ -2976,7 +2978,7 @@ describe('Service', () => { SwapVersion.Legacy, SwapType.Submarine, ), - ).toThrow(error.message); + ).rejects.toEqual(error); }, ); @@ -3010,8 +3012,8 @@ describe('Service', () => { ${21_000} | ${OrderSide.BUY} | ${Errors.EXCEED_MAXIMAL_AMOUNT(21_000, 10)} `( 'should throw when reverse amount is out of bounds', - ({ amount, side, error }) => { - expect(() => + async ({ amount, side, error }) => { + await expect( verifyAmount( pair, rate, @@ -3020,7 +3022,7 @@ describe('Service', () => { SwapVersion.Legacy, SwapType.ReverseSubmarine, ), - ).toThrow(error.message); + ).rejects.toEqual(error); }, ); }); @@ -3051,8 +3053,8 @@ describe('Service', () => { ${Number.MAX_SAFE_INTEGER} | ${Errors.EXCEED_MAXIMAL_AMOUNT(Number.MAX_SAFE_INTEGER, 1_000_000)} `( 'should throw when amount is out of bounds', - ({ amount, side, error }) => { - expect(() => + async ({ amount, side, error }) => { + await expect( verifyAmount( 'BTC/BTC', 1, @@ -3061,7 +3063,7 @@ describe('Service', () => { SwapVersion.Taproot, SwapType.Submarine, ), - ).toThrow(error.message); + ).rejects.toEqual(error); }, ); }); @@ -3093,8 +3095,8 @@ describe('Service', () => { ${Number.MAX_SAFE_INTEGER} | ${Errors.EXCEED_MAXIMAL_AMOUNT(Number.MAX_SAFE_INTEGER, 2_000_000)} `( 'should throw when amount is out of bounds', - ({ amount, side, error }) => { - expect(() => + async ({ amount, side, error }) => { + await expect( verifyAmount( 'BTC/BTC', 1, @@ -3103,7 +3105,7 @@ describe('Service', () => { SwapVersion.Taproot, SwapType.ReverseSubmarine, ), - ).toThrow(error.message); + ).rejects.toEqual(error); }, ); }); @@ -3136,8 +3138,8 @@ describe('Service', () => { ${Number.MAX_SAFE_INTEGER} | ${Errors.EXCEED_MAXIMAL_AMOUNT(Number.MAX_SAFE_INTEGER, 5_000_000)} `( 'should throw when amount is out of bounds', - ({ amount, side, error }) => { - expect(() => + async ({ amount, side, error }) => { + await expect( verifyAmount( 'BTC/BTC', 1, @@ -3146,15 +3148,15 @@ describe('Service', () => { SwapVersion.Taproot, SwapType.Chain, ), - ).toThrow(error.message); + ).rejects.toEqual(error); }, ); }); - test('should throw when pair cannot be found', () => { + test('should throw when pair cannot be found', async () => { const notFound = 'notFound'; - expect(() => + await expect( verifyAmount( notFound, 0, @@ -3163,7 +3165,7 @@ describe('Service', () => { SwapVersion.Legacy, SwapType.Submarine, ), - ).toThrow(Errors.PAIR_NOT_FOUND(notFound).message); + ).rejects.toEqual(Errors.PAIR_NOT_FOUND(notFound)); }); }); @@ -3248,10 +3250,13 @@ describe('Service', () => { ${OrderSide.SELL} | ${SwapType.Submarine} ${OrderSide.SELL} | ${SwapType.ReverseSubmarine} ${OrderSide.SELL} | ${SwapType.Chain} - `('should get legacy pair', ({ side, type }) => { - expect(getPair('BTC/BTC', side, SwapVersion.Legacy, type)).toEqual({ + `('should get legacy pair', async ({ side, type }) => { + await expect( + getPair('BTC/BTC', side, SwapVersion.Legacy, type), + ).resolves.toEqual({ base: 'BTC', quote: 'BTC', + referral: null, ...pairs.get('BTC/BTC'), }); }); @@ -3264,25 +3269,28 @@ describe('Service', () => { ${OrderSide.SELL} | ${SwapType.Submarine} | ${pairsTaprootSubmarine.get('BTC')!.get('BTC')} ${OrderSide.SELL} | ${SwapType.ReverseSubmarine} | ${pairsTaprootReverse.get('BTC')!.get('BTC')} ${OrderSide.SELL} | ${SwapType.Chain} | ${pairsTaprootChain.get('BTC')!.get('BTC')} - `('should get taproot pair', ({ side, type, expected }) => { - expect(getPair('BTC/BTC', side, SwapVersion.Taproot, type)).toEqual({ + `('should get taproot pair', async ({ side, type, expected }) => { + await expect( + getPair('BTC/BTC', side, SwapVersion.Taproot, type), + ).resolves.toEqual({ base: 'BTC', quote: 'BTC', + referral: null, ...expected, }); }); - test('should throw when pair cannot be found', () => { + test('should throw when pair cannot be found', async () => { const notFound = 'notFound'; - expect(() => + await expect( getPair( notFound, OrderSide.BUY, SwapVersion.Legacy, SwapType.Submarine, ), - ).toThrow(Errors.PAIR_NOT_FOUND(notFound).message); + ).rejects.toEqual(Errors.PAIR_NOT_FOUND(notFound)); }); });