raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
389 lines • 17.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.standardCalculator = exports.getStandardFeeCalculator = exports.imbalancePenaltyFee = exports.proportionalFee = exports.flatFee = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const bignumber_1 = require("@ethersproject/bignumber");
const constants_1 = require("@ethersproject/constants");
const decimal_js_1 = require("decimal.js");
const t = __importStar(require("io-ts"));
const utils_1 = require("../../channels/utils");
const types_1 = require("../../services/types");
const utils_2 = require("../../utils");
const types_2 = require("../../utils/types");
const Decimal = decimal_js_1.Decimal.clone({ precision: 40, rounding: decimal_js_1.Decimal.ROUND_HALF_EVEN });
const ZeroDec = new Decimal(0);
const OneDec = new Decimal(1);
/**
* Parses a number and throws if it isn't a number or isn't an integer
*
* @param value - Something which can be parsed as a number
* @returns integer
*/
function toInteger(value) {
const parsed = parseInt(value);
(0, utils_2.assert)(parsed !== undefined && parsed == value);
return parsed;
}
/**
* Creates a FeeFunc which can calculate the optimal fee based on the output capacity and input
* and output fee functions
*
* @param outCapacity - Oown capacity of output channel
* @param feeFuncs - Functions tuple
* @param feeFuncs."0" - Input channel's fee function
* @param feeFuncs."1" - Output channel's fee function
* @returns FeeFunction which calculates best fee from input amounts
*/
function makeFeeFunctionFromAmountIn(outCapacity, [inFeeFunc, outFeeFunc]) {
(0, utils_2.assert)(outCapacity.gt(0), 'no output channel capacity available');
return (amountIn_) => {
const amountIn = new Decimal(amountIn_.toHexString());
const feeIn = inFeeFunc(amountIn);
// https://raiden-network-specification.readthedocs.io/en/latest/mediation_fees.html#fee-calculation
const func = (amountOut) => feeIn.add(outFeeFunc(amountOut.neg())).sub(amountIn).add(amountOut);
let xA = ZeroDec;
let fA = func(xA);
(0, utils_2.assert)(fA.lt(0), 'input transfer not enough to pay minimum fees');
let xB = outCapacity;
let fB = func(xB);
(0, utils_2.assert)(fB.gte(0), 'output capacity not enough to mediate transfer');
// find interval containing root of monotonic func
while (true) {
const x = xB.add(xA).div(2);
const fX = func(x);
const slopeAX = fX.sub(fA).div(x.sub(xA));
const slopeXB = fB.sub(fX).div(xB.sub(x));
// once slope of both stretches are close enough (because Δx→dx or because it's
// a piecewise linear function), we break and calculate the linear root of segment
if (slopeXB.sub(slopeAX).abs().lt(0.000001))
break;
// else, we contine with the stretch which contains the root (y of opposite signals)
else if (fA.isNegative() !== fX.isNegative()) {
xB = x;
fB = fX;
}
else {
xA = x;
fA = fX;
}
}
// calculate x where line [(xA, fA), (xB, fB)] crosses y=0:
// x₀ = xA - yA/Δ, Δ=(yB-yA)/(xB-xA)
const amountOut = xA.sub(fA.mul(xB.sub(xA)).div(fB.sub(fA)));
// return fee = amountIn - amountOut
return (0, types_2.decode)(types_1.Fee, amountIn.sub(amountOut).toFixed(0, Decimal.ROUND_HALF_EVEN));
};
}
exports.flatFee = {
name: 'flat',
emptySchedule: { flat: constants_1.Zero },
decodeConfig(config, defaultConfig) {
// flat config uses 'half' the per-token config, one half for each channel [in, out]
return (0, types_2.decode)(types_1.Fee, config ?? defaultConfig).div(2);
},
channelFee(flat) {
const flatFee = new Decimal(flat.toHexString());
return () => flatFee;
},
fee(flat) {
// flat fee just sums once for each channel
const res = (0, types_2.decode)(types_1.Fee, flat.mul(2));
return () => res;
},
schedule(flat) {
return { flat };
},
};
exports.proportionalFee = {
name: 'proportional',
emptySchedule: { proportional: constants_1.Zero },
decodeConfig(config, defaultConfig) {
// https://raiden-network-specification.readthedocs.io/en/latest/mediation_fees.html#converting-per-hop-proportional-fees-in-per-channel-proportional-fees
// 1M is because config is received and returned as parts-per-million integers
const perHopRatio = new Decimal(toInteger(config ?? defaultConfig)).div(1e6);
const perChannelPPM = perHopRatio.div(perHopRatio.add(2)).mul(1e6);
return perChannelPPM.toDecimalPlaces(0, Decimal.ROUND_HALF_EVEN).toNumber();
},
channelFee(perChannelPPM) {
const perChannelRatio = new Decimal(perChannelPPM).div(1e6);
return (amount) => amount.mul(perChannelRatio).abs();
},
fee(perChannelPPM) {
const perChannelRatio = new Decimal(perChannelPPM).div(1e6);
return (amountIn_) => {
const amountIn = new Decimal(amountIn_.toHexString());
// for proportional fees only: xout = xin*(1-q)/(1+q)
const amountOut = amountIn.mul(OneDec.sub(perChannelRatio).div(OneDec.add(perChannelRatio)));
const fee = amountIn.sub(amountOut);
return (0, types_2.decode)(types_1.Fee, fee.toFixed(0, Decimal.ROUND_HALF_EVEN));
};
},
schedule(perChannelPPM) {
return { proportional: bignumber_1.BigNumber.from(perChannelPPM) };
},
};
/**
* Creates an array of [count] BNs, starting with [start], ending with [end], and
* intermediary values being the left edges for stretches (splits) of length differing at most 1.
* Count must be greater than or equal 2 (start and end), and must be smaller or equal range width
*
* @param start - Start value
* @param end - End value
* @param count - Values count, which will divide range into [count - 1] stretches
* @returns Sorted array of BNs of length [count]
*/
function linspace(start, end, count) {
const width = end.sub(start);
(0, utils_2.assert)(count >= 2 && width.gte(count - 1), 'invalid linspace params');
const ranges = count - 1;
const step = new Decimal(width.toString()).div(ranges);
const result = [start];
for (let i = 1; i < ranges; i++) {
result.push(start.add(step.mul(i).toFixed(0, Decimal.ROUND_HALF_EVEN)));
}
return [...result, end];
}
/**
* Finds the rightmost index for which arr[i] <= x < arr[i+1] (<= arr[arr.length-1])
* Performs a binary search, with complexity O(log(N))
* It first estimates a "good bet" of index if the func is equally spaced, then offsets it back or
* forth until x is in between range
*
* @param x - Point to find index for
* @param func - Discrete func
* @returns Found index
*/
function findRangeIndex(x, func) {
(0, utils_2.assert)(func.length >= 2, 'invalid linspace');
const x0 = func[0][0];
const xL = func[func.length - 1][0]; // x_last
// special-case where x is exactly over xLast, make it part of last stretch instead of beyond it
if (x.eq(xL.toHexString()))
return func.length - 2;
const width = new Decimal(xL.sub(x0).toHexString());
const step = width.div(func.length - 1);
let index = Math.floor(x.sub(x0.toHexString()).div(step).toNumber());
let offs = 0;
do {
if (index < 0)
return -1;
else if (index >= func.length - 1)
return func.length - 1;
if (x.gte(func[index + 1][0].toHexString()))
offs = -1;
else if (x.lt(func[index][0].toHexString()))
offs = 1;
else
offs = 0;
index += offs;
} while (offs);
return index;
}
function interpolate(x, [x0, y0], [x1, y1]) {
// y = f(x) = (x-x0)*Δy/Δx + y0
return x
.sub(x0.toHexString())
.mul(y1.sub(y0).toHexString())
.div(x1.sub(x0).toHexString())
.add(y0.toHexString());
}
/**
* Get the value of the piecewise linear curve described by the pairs in [arr] at point [x]
*
* @param x - Point where to get the value for
* @param arr - function described as array of [x, y] pairs containing points linked by lines
* @returns function value at point x
*/
function interpolateFunc(x, arr) {
let index = findRangeIndex(x, arr);
// in case index before|beyond arr limits, use first|last stretches
if (index < 0)
index = 0;
else if (index >= arr.length - 1)
index = arr.length - 2;
return interpolate(x, arr[index], arr[index + 1]);
}
/**
* Calculates pair of points discretizing a U-shaped curve which describes channel's imbalance fees
* for a given total capacity and config.
*
* @param channelCapacity - Channel's total capacity (sum of deposits from both ends)
* @param proportionalImbalanceFee - Fee config, in PPM (1% = 10,000)
* @returns array of [x, y] pairs, where x is the old/new channel capacity and y is the fee
*/
function calculatePenaltyFunction(channelCapacity, proportionalImbalanceFee) {
const NUM_DISCRETISATION_POINTS = 21;
const MAX_SLOPE = new Decimal('0.1');
(0, utils_2.assert)(channelCapacity.gt(0), 'not enough capacity');
const channelCapacityDec = new Decimal(channelCapacity.toHexString());
const numBasePoints = Decimal.min(NUM_DISCRETISATION_POINTS, channelCapacityDec.add(1)).toNumber();
const xValues = linspace(constants_1.Zero, channelCapacity, numBasePoints);
if (proportionalImbalanceFee === 0) {
return [
[constants_1.Zero, constants_1.Zero],
[channelCapacity, constants_1.Zero],
];
}
const proportionalImbalanceFeeDec = new Decimal(proportionalImbalanceFee).div(1e6);
(0, utils_2.assert)(proportionalImbalanceFeeDec.lte(MAX_SLOPE.div(2)), 'Too high imbalance fee');
const maxImbalanceFee = channelCapacityDec.mul(proportionalImbalanceFeeDec);
const s = MAX_SLOPE;
const c = maxImbalanceFee;
const o = channelCapacityDec.div(2);
const b = Decimal.min(10, s.mul(o).div(c));
const func = (x) => bignumber_1.BigNumber.from(new Decimal(x.toHexString())
.sub(o)
.abs()
.pow(b)
.mul(c)
.div(o.pow(b))
.toFixed(0, Decimal.ROUND_HALF_EVEN));
// TS can't properly type the map to tuple, even if the input has the correct type
return xValues.map((x) => [x, func(x)]);
}
exports.imbalancePenaltyFee = {
name: 'imbalance',
emptySchedule: { imbalance_penalty: null },
decodeConfig(config, defaultConfig) {
const imbalancePPM = toInteger(config ?? defaultConfig);
(0, utils_2.assert)(imbalancePPM >= 0 && imbalancePPM <= 50000, 'Too high imbalance fee');
return imbalancePPM;
},
channelFee(imbalancePPM, channel) {
const { totalCapacity, ownCapacity } = (0, utils_1.channelAmounts)(channel);
if (!imbalancePPM || !totalCapacity.gt(0))
return () => ZeroDec;
const discreteFunc = calculatePenaltyFunction(totalCapacity, imbalancePPM);
const ipAtCurCapacity = interpolateFunc(new Decimal(ownCapacity.toHexString()), discreteFunc);
return (amount) => interpolateFunc(amount.add(ownCapacity.toHexString()), discreteFunc).sub(ipAtCurCapacity);
},
fee(imbalancePPM, channelIn, channelOut) {
const channelInFeeFunc = this.channelFee(imbalancePPM, channelIn);
const channelOutFeeFunc = this.channelFee(imbalancePPM, channelOut);
const outOwnCapacity = new Decimal((0, utils_1.channelAmounts)(channelOut).ownCapacity.toHexString());
return makeFeeFunctionFromAmountIn(outOwnCapacity, [channelInFeeFunc, channelOutFeeFunc]);
},
schedule(imbalancePPM, channel) {
const { totalCapacity } = (0, utils_1.channelAmounts)(channel);
if (!imbalancePPM || !totalCapacity.gt(0))
return { imbalance_penalty: null };
const discreteFunc = calculatePenaltyFunction(totalCapacity, imbalancePPM);
return { imbalance_penalty: discreteFunc };
},
};
/**
* Returns a standard mediation FeeModel which translates a per-token mapping of feeModels to a
* model which validates each per-key config and calculates the fees sum
*
* Config is expected and decoded as being a indexed mapping object where keys are token addresses,
* and values are objects containing optional config properties where keys are the models keys
* passed as parameter and values are each FeeModel expected and validated config.
* e.g.:
* - models={ flat: flatFee<number> };
* - expectedConfig={ [token: Address]: { flat: number } }
*
* Additionally, if an [AddressZero] token config is present, it'll be used as fallback/default
* config in case the requested token isn't set.
*
* @param models - Models dict where [key] is that model's config name on the per-token config obj
* @returns Standard Fee calculator
*/
function getStandardFeeCalculator(models) {
const emptySchedule = Object.assign({ cap_fees: true }, ...Object.values(models).map((model) => model.emptySchedule));
const mapCodec = t.union([t.undefined, t.record(t.string, t.record(t.string, t.unknown))]);
const standardCalculator = {
name: 'standard',
emptySchedule,
decodeConfig(config, defaultConfig) {
const tokenConfigMap = {
...(0, types_2.decode)(mapCodec, config),
...(0, types_2.decode)(mapCodec, defaultConfig),
};
for (const [token_, config] of Object.entries(tokenConfigMap)) {
const token = (0, types_2.decode)(types_2.Address, token_);
const perTokenConfig = { cap: config['cap'] ?? true };
for (const [key_, model] of Object.entries(models)) {
const key = key_;
if (!(key in config))
continue;
perTokenConfig[key] = model.decodeConfig(config[key_]);
}
tokenConfigMap[token] = perTokenConfig;
}
return tokenConfigMap;
},
channelFee(config, channel) {
const tokenAddr = channel.token;
const perTokenConfig = config[tokenAddr] ?? config[constants_1.AddressZero];
const channelFeeFuncs = Object.entries(models)
.map(([key, model]) => {
const modelConfig = perTokenConfig?.[key];
if (modelConfig)
return model.channelFee(modelConfig, channel);
})
.filter(types_2.isntNil);
return (amount) => channelFeeFuncs.reduce((fee, func) => fee.add(func(amount)), ZeroDec);
},
fee(config, channelIn, channelOut) {
const channelInFeeFunc = this.channelFee(config, channelIn);
const channelOutFeeFunc = this.channelFee(config, channelOut);
const outOwnCapacity = new Decimal((0, utils_1.channelAmounts)(channelOut).ownCapacity.toHexString());
const capFees = (config[channelIn.token] ?? config[constants_1.AddressZero])?.cap ?? emptySchedule.cap_fees;
const feeFunction = makeFeeFunctionFromAmountIn(outOwnCapacity, [
channelInFeeFunc,
channelOutFeeFunc,
]);
return (amountIn) => {
let fee = feeFunction(amountIn);
if (capFees && fee.lt(0))
fee = constants_1.Zero; // cap fee
return fee;
};
},
schedule(config, channel) {
const tokenAddr = channel.token;
const perTokenConfig = config[tokenAddr] ?? config[constants_1.AddressZero];
return Object.assign({}, emptySchedule, perTokenConfig?.cap !== undefined ? { cap_fees: perTokenConfig.cap } : undefined, ...Object.entries(models).map(([key_, model]) => {
const key = key_;
const modelConfig = perTokenConfig?.[key];
if (!modelConfig)
return {};
return model.schedule(modelConfig, channel);
}));
},
};
return standardCalculator;
}
exports.getStandardFeeCalculator = getStandardFeeCalculator;
exports.standardCalculator = getStandardFeeCalculator({
flat: exports.flatFee,
proportional: exports.proportionalFee,
imbalance: exports.imbalancePenaltyFee,
});
// type StandardConfig = ConfigOf<typeof standardCalculator>;
// type StandardPerTokenConfig = StandardConfig[string];
// type StandardSchedule = ScheduleOf<typeof standardCalculator>;
//# sourceMappingURL=types.js.map