Skip to content
This repository has been archived by the owner on Aug 28, 2019. It is now read-only.

Commit

Permalink
feat: new fee structure
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Mar 28, 2019
1 parent 0826ad4 commit 7e3350a
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 34 deletions.
6 changes: 6 additions & 0 deletions lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,22 @@ class Config {
{
base: 'LTC',
quote: 'BTC',

fee: 1,
},
{
base: 'BTC',
quote: 'BTC',

fee: 0.5,
rate: 1,
},
{
base: 'LTC',
quote: 'LTC',

rate: 1,
fee: 0.5,
},
];
}
Expand Down
13 changes: 13 additions & 0 deletions lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,16 @@ export const getSuccessfulTrades = async (swapRepository: SwapRepository, revers
reverseSwaps,
};
};

/**
* Converts the reponse of the backend method "getFeeEstimation" to an object
*/
export const feeMapToObject = (feesMap: [string, number][]) => {
const response: any = {};

feesMap.forEach(([symbol, fee]) => {
response[symbol] = fee;
});

return response;
};
11 changes: 11 additions & 0 deletions lib/consts/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ export type CurrencyConfig = {
minLocalBalance: number;
minRemoteBalance: number;
};

export type PairConfig = {
base: string;
quote: string;

// Percentage of the amount that will be charged as fee
fee?: number;

// If there is a hardcoded rate the CryptoCompare API will not be queried
rate?: number;
};
45 changes: 45 additions & 0 deletions lib/rates/FeeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Logger from '../Logger';
import { PairConfig } from '../consts/Types';
import BoltzClient from '../boltz/BoltzClient';
import { mapToObject, getPairId, feeMapToObject, stringify } from '../Utils';

class FeeProvider {
// A map between the symbols of the pairs and their percentage fees
private percentageFees = new Map<string, number>();

constructor(private logger: Logger, private boltz: BoltzClient) {}

public init = (pairs: PairConfig[]) => {
pairs.forEach((pair) => {
// Set the configured fee or fallback to 1% if it is not defined
const percentage = pair.fee !== undefined ? pair.fee : 1;
this.percentageFees.set(getPairId(pair), percentage / 100);
});

this.logger.debug(`Prepared data for fee estimations: ${stringify(mapToObject(this.percentageFees))}`);
}

public getFee = async (pair: string, chainCurrency: string, amount: number, isReverse: boolean) => {
const feeReponse = await this.boltz.getFeeEstimation(chainCurrency);
const feeMap = feeMapToObject(feeReponse.feesMap);

const baseFee = this.getBaseFee(feeMap[chainCurrency], isReverse);
const percentageFee = Math.ceil(this.percentageFees.get(pair)! * amount);

return baseFee + percentageFee;
}

private getBaseFee = (satPerVbyte: number, isReverse: boolean) => {
if (isReverse) {
// The lockup transaction which spends a P2WPKH output (possibly more but we assume a best case scenario here),
// locks up funds in a P2WSH swap and sends the change back to a P2WKH output has about 153 vbytes
return satPerVbyte * 153;
} else {
// The claim transaction which spends a nested SegWit swap output and
// sends it to a bech32 P2WPKH address has about 140 vbytes
return satPerVbyte * 140;
}
}
}

export default FeeProvider;
2 changes: 1 addition & 1 deletion lib/rates/RateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class RateProvider {
}

/**
* Gets a map of of rates for the provided pairs
* Gets a map of of rates from CryptoCompare for the provided pairs
*/
public init = async (pairs: PairInstance[]) => {
pairs.forEach((pair) => {
Expand Down
4 changes: 2 additions & 2 deletions lib/service/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export default {
message: `${amount} is less than minimal ${minimalAmount}`,
code: concatErrorCode(ErrorCodePrefix.Service, 4),
}),
SWAP_WITH_INVOICE_EXISTS_ALREADY: (invoice: string): Error => ({
message: `a swap with the invoice ${invoice} exists already`,
SWAP_WITH_INVOICE_EXISTS_ALREADY: (): Error => ({
message: 'a swap with this invoice exists already',
code: concatErrorCode(ErrorCodePrefix.Service, 5),
}),
};
38 changes: 12 additions & 26 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@ import Database from '../db/Database';
import SwapRepository from './SwapRepository';
import PairRepository from './PairRepository';
import BoltzClient from '../boltz/BoltzClient';
import FeeProvider from '../rates/FeeProvider';
import RateProvider from '../rates/RateProvider';
import { SwapUpdateEvent } from '../consts/Enums';
import { encodeBip21 } from './PaymentRequestUtils';
import ReverseSwapRepository from './ReverseSwapRepository';
import { SwapUpdate, CurrencyConfig } from '../consts/Types';
import { SwapUpdate, CurrencyConfig, PairConfig } from '../consts/Types';
import { OrderSide, OutputType, CurrencyInfo } from '../proto/boltzrpc_pb';
import { splitPairId, stringify, generateId, mapToObject, satoshisToCoins } from '../Utils';
import { PairInstance, PairFactory, SwapInstance, ReverseSwapInstance } from '../consts/Database';

type PairConfig = {
base: string;
quote: string;

// If there is a hardcoded rate the CryptoCompare API will not be queried
rate?: number;
};
import { splitPairId, stringify, generateId, mapToObject, satoshisToCoins, feeMapToObject } from '../Utils';

type Pair = {
id: string;
Expand All @@ -44,6 +37,7 @@ class Service extends EventEmitter {

private pairRepository: PairRepository;

private feeProvider: FeeProvider;
private rateProvider: RateProvider;

private pairs = new Map<string, Pair>();
Expand All @@ -61,6 +55,7 @@ class Service extends EventEmitter {
this.swapRepository = new SwapRepository(db.models);
this.reverseSwapRepository = new ReverseSwapRepository(db.models);

this.feeProvider = new FeeProvider(this.logger, this.boltz);
this.rateProvider = new RateProvider(this.logger, rateInterval, currencies);
}

Expand All @@ -73,10 +68,10 @@ class Service extends EventEmitter {
const isUndefinedOrNull = (value: any) => value === undefined || value === null;

const comparePairArrays = (array: PairArray, compare: PairArray, callback: Function) => {
array.forEach((pair) => {
array.forEach((pair: PairConfig | PairInstance) => {
let inCompare = false;

compare.forEach((comparePair) => {
compare.forEach((comparePair: PairConfig | PairInstance) => {
if (pair.base === comparePair.base &&
pair.quote === comparePair.quote) {

Expand Down Expand Up @@ -130,6 +125,7 @@ class Service extends EventEmitter {
}
};

this.feeProvider.init(pairs);
await this.rateProvider.init(dbPairs);

dbPairs.forEach((pair) => {
Expand Down Expand Up @@ -175,16 +171,6 @@ class Service extends EventEmitter {
public getFeeEstimation = async () => {
const feeEstimation = await this.boltz.getFeeEstimation('', 2);

const feeMapToObject = (feesMap: [string, number][]) => {
const response: any = {};

feesMap.forEach(([symbol, fee]) => {
response[symbol] = fee;
});

return response;
};

return feeMapToObject(feeEstimation.feesMap);
}

Expand Down Expand Up @@ -218,7 +204,7 @@ class Service extends EventEmitter {
const satoshi = Number(millisatoshis) / 1000;

this.verifyAmount(satoshi, pairId, side, false, rate);
const fee = Math.ceil(10 + (satoshi * 0.01));
const fee = await this.feeProvider.getFee(pairId, chainCurrency, satoshi, false);

const {
address,
Expand All @@ -240,7 +226,7 @@ class Service extends EventEmitter {
lockupAddress: address,
});
} catch (error) {
throw Errors.SWAP_WITH_INVOICE_EXISTS_ALREADY(invoice);
throw Errors.SWAP_WITH_INVOICE_EXISTS_ALREADY();
}

return {
Expand All @@ -265,9 +251,10 @@ class Service extends EventEmitter {
const { base, quote, rate } = this.getPair(pairId);

const side = this.getOrderSide(orderSide);
const chainCurrency = side === OrderSide.BUY ? base : quote;

this.verifyAmount(amount, pairId, side, true, rate);
const fee = Math.ceil(1000 + (amount * 0.01));
const fee = await this.feeProvider.getFee(pairId, chainCurrency, amount, true);

const {
invoice,
Expand All @@ -277,7 +264,6 @@ class Service extends EventEmitter {
lockupTransactionHash,
} = await this.boltz.createReverseSwap(base, quote, side, rate, fee, claimPublicKey, amount);

const chainCurrency = side === OrderSide.BUY ? base : quote;
await this.boltz.listenOnAddress(chainCurrency, lockupAddress);

const id = generateId(6);
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"lint": "tslint --project tsconfig.json && tslint --config tslint-alt.json 'bin/*' 'test/**/*.ts'",
"lint:fix": "tslint --fix --project tsconfig.json && tslint --config tslint-alt.json 'bin/*' 'test/**/*.ts'",
"test": "npm run test:unit",
"test:unit": "mocha test/unit/*.spec.ts"
"test:unit": "mocha test/unit/*.spec.ts test/unit/rates/*.spec.ts"
},
"cross-os": {
"precompile": {
Expand Down Expand Up @@ -69,6 +69,7 @@
"grpc_tools_node_protoc_ts": "^2.5.0",
"mocha": "^5.2.0",
"nodemon": "^1.18.10",
"ts-mockito": "^2.3.1",
"ts-node": "^7.0.1",
"tslint": "^5.14.0",
"tslint-config-airbnb": "^5.11.1",
Expand Down
77 changes: 77 additions & 0 deletions test/unit/rates/FeeProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect } from 'chai';
import { mock, when, instance, anyString } from 'ts-mockito';
import Logger from '../../../lib/Logger';
import FeeProvider from '../../../lib/rates/FeeProvider';
import BoltzClient from '../../../lib/boltz/BoltzClient';

describe('FeeProvider', () => {
const btcFee = 36;
const ltcFee = 3;

const boltzMock = mock(BoltzClient);
when(boltzMock.getFeeEstimation(anyString())).thenResolve({
feesMap: [
['BTC', btcFee],
['LTC', ltcFee],
],
});

const feeProvider = new FeeProvider(Logger.disabledLogger, instance(boltzMock));

it('should init', () => {
feeProvider.init([
{
base: 'LTC',
quote: 'BTC',
fee: 0.5,
},
{
base: 'BTC',
quote: 'BTC',
fee: 0,
},
{
base: 'LTC',
quote: 'LTC',

// The FeeProvider should set this to 1
fee: undefined,
},
]);

const feeMap = feeProvider['percentageFees'];
expect(feeMap.size).to.be.equal(3);

expect(feeMap.get('LTC/BTC')).to.be.equal(0.005);
expect(feeMap.get('BTC/BTC')).to.be.equal(0);
expect(feeMap.get('LTC/LTC')).to.be.equal(0.01);
});

it('should estimate fees', async () => {
const results = await Promise.all([
feeProvider.getFee('LTC/BTC', 'BTC', 100000, false),
feeProvider.getFee('LTC/BTC', 'LTC', 532100, false),

feeProvider.getFee('BTC/BTC', 'BTC', 100000, false),
feeProvider.getFee('BTC/BTC', 'BTC', 100000, true),

feeProvider.getFee('LTC/LTC', 'LTC', 987654321, false),
feeProvider.getFee('LTC/LTC', 'LTC', 987654321, true),
]);

const expected = [
5540,
3081,

5040,
5508,

9876964,
9877003,
];

results.forEach((result, index) => {
expect(result).to.be.equal(expected[index]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { expect } from 'chai';
import Logger from '../../lib/Logger';
import Database from '../../lib/db/Database';
import RateProvider from '../../lib/rates/RateProvider';
import PairRepository from '../../lib/service/PairRepository';
import Logger from '../../../lib/Logger';
import Database from '../../../lib/db/Database';
import RateProvider from '../../../lib/rates/RateProvider';
import PairRepository from '../../../lib/service/PairRepository';

describe('RateProvider', () => {
const decimals = 100000000;
Expand Down

0 comments on commit 7e3350a

Please sign in to comment.