Skip to content

Commit

Permalink
feat: nested extra fees (#826)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Feb 20, 2025
1 parent 49bf13e commit 64fba5a
Show file tree
Hide file tree
Showing 15 changed files with 1,047 additions and 101 deletions.
2 changes: 2 additions & 0 deletions lib/api/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export default {
`${symbol} does not support ${argName}`,
INVALID_SWAP_STATUS: (status: string): string =>
`invalid swap status: ${status}`,
INVALID_EXTRA_FEES_PERCENTAGE: (percentage: number): string =>
`invalid extra fees percentage: ${percentage}`,
};
12 changes: 12 additions & 0 deletions lib/api/v2/routers/ReferralRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Logger from '../../../Logger';
import ReferralStats from '../../../data/ReferralStats';
import Stats from '../../../data/Stats';
import Referral from '../../../db/models/Referral';
import ExtraFeeRepository from '../../../db/repositories/ExtraFeeRepository';
import Bouncer from '../../Bouncer';
import { errorResponse, successResponse } from '../../Utils';
import RouterBase from './RouterBase';
Expand Down Expand Up @@ -203,6 +204,8 @@ class ReferralRouter extends RouterBase {
*/
router.get('/stats', this.handleError(this.getStats));

router.get('/stats/extra', this.handleError(this.getExtraFees));

return router;
};

Expand Down Expand Up @@ -233,6 +236,15 @@ class ReferralRouter extends RouterBase {
successResponse(res, await Stats.generate(0, 0, referral.id));
};

private getExtraFees = async (req: Request, res: Response) => {
const referral = await this.checkAuthentication(req, res);
if (referral === undefined) {
return;
}

successResponse(res, await ExtraFeeRepository.getStats(referral.id));
};

private checkAuthentication = async (
req: Request,
res: Response,
Expand Down
43 changes: 40 additions & 3 deletions lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ReferralRepository from '../../../db/repositories/ReferralRepository';
import SwapRepository from '../../../db/repositories/SwapRepository';
import RateProviderTaproot from '../../../rates/providers/RateProviderTaproot';
import Errors from '../../../service/Errors';
import Service, { WebHookData } from '../../../service/Service';
import Service, { ExtraFees, WebHookData } from '../../../service/Service';
import ChainSwapSigner from '../../../service/cooperative/ChainSwapSigner';
import MusigSigner, {
PartialSignature,
Expand Down Expand Up @@ -1669,19 +1669,21 @@ class SwapRouter extends RouterBase {
};

private createSubmarine = async (req: Request, res: Response) => {
const { to, from, invoice, webhook, pairHash, refundPublicKey } =
const { to, from, invoice, webhook, pairHash, refundPublicKey, extraFees } =
validateRequest(req.body, [
{ name: 'to', type: 'string' },
{ name: 'from', type: 'string' },
{ name: 'webhook', type: 'object', optional: true },
{ name: 'invoice', type: 'string', optional: true },
{ name: 'pairHash', type: 'string', optional: true },
{ name: 'extraFees', type: 'object', optional: true },
{ name: 'refundPublicKey', type: 'string', hex: true, optional: true },
]);
const referralId = parseReferralId(req);

const { pairId, orderSide } = this.service.convertToPairAndSide(from, to);
const webHookData = this.parseWebHook(webhook);
const extraFeesData = this.parseExtraFees(extraFees);

let response: { id: string };

Expand All @@ -1696,6 +1698,7 @@ class SwapRouter extends RouterBase {
undefined,
SwapVersion.Taproot,
webHookData,
extraFeesData,
);
} else {
const { preimageHash } = validateRequest(req.body, [
Expand Down Expand Up @@ -1726,15 +1729,19 @@ class SwapRouter extends RouterBase {
const { id } = validateRequest(req.params, [
{ name: 'id', type: 'string' },
]);
const { invoice, pairHash } = validateRequest(req.body, [
const { invoice, extraFees, pairHash } = validateRequest(req.body, [
{ name: 'invoice', type: 'string' },
{ name: 'pairHash', type: 'string', optional: true },
{ name: 'extraFees', type: 'object', optional: true },
]);

const extraFeesData = this.parseExtraFees(extraFees);

const response = await this.service.setInvoice(
id,
invoice.toLowerCase(),
pairHash,
extraFeesData,
);
successResponse(res, response);
};
Expand Down Expand Up @@ -1841,6 +1848,7 @@ class SwapRouter extends RouterBase {
webhook,
address,
pairHash,
extraFees,
description,
routingNode,
preimageHash,
Expand All @@ -1859,6 +1867,7 @@ class SwapRouter extends RouterBase {
{ name: 'address', type: 'string', optional: true },
{ name: 'webhook', type: 'object', optional: true },
{ name: 'pairHash', type: 'string', optional: true },
{ name: 'extraFees', type: 'object', optional: true },
{ name: 'description', type: 'string', optional: true },
{ name: 'routingNode', type: 'string', optional: true },
{ name: 'claimAddress', type: 'string', optional: true },
Expand All @@ -1876,6 +1885,7 @@ class SwapRouter extends RouterBase {

const { pairId, orderSide } = this.service.convertToPairAndSide(from, to);
const webHookData = this.parseWebHook(webhook);
const extraFeesData = this.parseExtraFees(extraFees);

const response = await this.service.createReverseSwap({
pairId,
Expand All @@ -1895,6 +1905,7 @@ class SwapRouter extends RouterBase {
userAddress: address,
webHook: webHookData,
prepayMinerFee: false,
extraFees: extraFeesData,
version: SwapVersion.Taproot,
userAddressSignature: addressSignature,
});
Expand Down Expand Up @@ -2006,6 +2017,7 @@ class SwapRouter extends RouterBase {
from,
webhook,
pairHash,
extraFees,
referralId,
preimageHash,
claimAddress,
Expand All @@ -2019,6 +2031,7 @@ class SwapRouter extends RouterBase {
{ name: 'webhook', type: 'object', optional: true },
{ name: 'preimageHash', type: 'string', hex: true },
{ name: 'pairHash', type: 'string', optional: true },
{ name: 'extraFees', type: 'object', optional: true },
{ name: 'referralId', type: 'string', optional: true },
{ name: 'claimAddress', type: 'string', optional: true },
{ name: 'userLockAmount', type: 'number', optional: true },
Expand All @@ -2029,6 +2042,7 @@ class SwapRouter extends RouterBase {

checkPreimageHashLength(preimageHash);
const webHookData = this.parseWebHook(webhook);
const extraFeesData = this.parseExtraFees(extraFees);

const { pairId, orderSide } = this.service.convertToPairAndSide(from, to);
const response = await this.service.createChainSwap({
Expand All @@ -2043,6 +2057,7 @@ class SwapRouter extends RouterBase {
refundPublicKey,
serverLockAmount,
webHook: webHookData,
extraFees: extraFeesData,
});

await markSwap(this.service.sidecar, req.ip, response.id);
Expand Down Expand Up @@ -2279,6 +2294,28 @@ class SwapRouter extends RouterBase {
return res;
};

private parseExtraFees = (
data: Record<string, any> | undefined,
): ExtraFees | undefined => {
if (data === undefined) {
return undefined;
}

const res = validateRequest(data, [
{ name: 'id', type: 'string' },
{ name: 'percentage', type: 'number' },
]);

if (res.percentage <= 0 || res.percentage > 10) {
throw ApiErrors.INVALID_EXTRA_FEES_PERCENTAGE(res.percentage);
}

return {
id: res.id,
percentage: res.percentage,
};
};

private getReferralFromHeader = async (req: Request) => {
const referral = req.header('referral');
if (referral === undefined) {
Expand Down
3 changes: 3 additions & 0 deletions lib/db/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ChainSwapData from './models/ChainSwapData';
import ChainTip from './models/ChainTip';
import ChannelCreation from './models/ChannelCreation';
import DatabaseVersion from './models/DatabaseVersion';
import ExtraFee from './models/ExtraFee';
import KeyProvider from './models/KeyProvider';
import LightningPayment from './models/LightningPayment';
import MarkedSwap from './models/MarkedSwap';
Expand Down Expand Up @@ -96,6 +97,7 @@ class Database {
Pair.sync(),
ChainTip.sync(),
Referral.sync(),
ExtraFee.sync(),
KeyProvider.sync(),
Rebroadcast.sync(),
DatabaseVersion.sync(),
Expand Down Expand Up @@ -130,6 +132,7 @@ class Database {

private loadModels = () => {
Pair.load(Database.sequelize);
ExtraFee.load(Database.sequelize);
Referral.load(Database.sequelize);
Swap.load(Database.sequelize);
TransactionLabel.load(Database.sequelize);
Expand Down
55 changes: 55 additions & 0 deletions lib/db/models/ExtraFee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { DataTypes, Model, Sequelize } from 'sequelize';

type ExtraFeeType = {
swapId: string;
id: string;
fee: number;
percentage: number;
};

class ExtraFee extends Model implements ExtraFeeType {
public swapId!: string;
public id!: string;
public fee!: number;
public percentage!: number;

public createdAt!: Date;
public updatedAt!: Date;

public static load = (sequelize: Sequelize): void => {
ExtraFee.init(
{
swapId: {
type: new DataTypes.STRING(255),
primaryKey: true,
allowNull: false,
},
id: {
type: new DataTypes.STRING(255),
allowNull: false,
},
fee: {
type: new DataTypes.BIGINT(),
allowNull: true,
},
percentage: {
type: new DataTypes.DECIMAL(),
allowNull: false,
},
},
{
sequelize,
tableName: 'extra_fees',
indexes: [
{
unique: false,
fields: ['id'],
},
],
},
);
};
}

export default ExtraFee;
export { ExtraFeeType };
78 changes: 78 additions & 0 deletions lib/db/repositories/ExtraFeeRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { QueryTypes } from 'sequelize';
import { SwapUpdateEvent } from '../../consts/Enums';
import { getNestedObject } from '../../data/Utils';
import Database from '../Database';
import ExtraFee, { ExtraFeeType } from '../models/ExtraFee';

type GroupedByYearMonth<T> = Record<string, Record<string, T>>;

class ExtraFeeRepository {
private static readonly statsQuery = `
WITH successful AS (SELECT id, status, referral, "createdAt"
FROm swaps
WHERE status = ?
UNION ALL
SELECT id, status, referral, "createdAt"
FROM "reverseSwaps"
WHERE status = ?
UNION ALL
SELECT id, status, referral, "createdAt"
FROM "chainSwaps"
WHERE status = ?),
successful_extra AS (SELECT e.id AS id, e.fee AS fee, e."createdAt"
FROM successful s
RIGHT JOIN extra_fees e on s.id = e."swapId"
WHERE referral = ?)
SELECT EXTRACT(YEAR FROM "createdAt") AS year, EXTRACT(MONTH FROM "createdAt") AS month, id, SUM(fee) AS fee
FROM successful_extra
GROUP BY year, month, id
ORDER BY year, month, id;
`;

public static create = async (
extraFee: Omit<ExtraFeeType, 'fee'> & { fee?: number },
): Promise<void> => {
await ExtraFee.create(extraFee);
};

public static get = async (id: string): Promise<ExtraFeeType | null> => {
return await ExtraFee.findByPk(id);
};

public static setFee = async (id: string, fee: number): Promise<void> => {
await ExtraFee.update({ fee }, { where: { swapId: id } });
};

public static getStats = async (
id: string,
): Promise<GroupedByYearMonth<number>> => {
const stats = (await Database.sequelize.query(
{
query: ExtraFeeRepository.statsQuery,
values: [
SwapUpdateEvent.TransactionClaimed,
SwapUpdateEvent.InvoiceSettled,
SwapUpdateEvent.TransactionClaimed,
id,
],
},
{
type: QueryTypes.SELECT,
},
)) as { year: number; month: number; id: string; fee: number }[];

const res = {};

stats.forEach((stat) => {
const monthObj = getNestedObject(
getNestedObject(res, stat.year),
stat.month,
);
monthObj[stat.id] = Number(stat.fee);
});

return res;
};
}

export default ExtraFeeRepository;
Loading

0 comments on commit 64fba5a

Please sign in to comment.