1661 lines
61 KiB
TypeScript
1661 lines
61 KiB
TypeScript
import { BigNumber } from '@0x/utils';
|
|
import { anything, instance, mock, when } from 'ts-mockito';
|
|
|
|
import { DEFAULT_FEE_MODEL_CONFIGURATION, FeeModelConfiguration } from '../../src/config';
|
|
import {
|
|
BPS_TO_RATIO,
|
|
DEFAULT_MIN_EXPIRY_DURATION_MS,
|
|
RFQM_TX_OTC_ORDER_GAS_ESTIMATE,
|
|
RFQT_GAS_IMPROVEMENT,
|
|
RFQT_GAS_IMPROVEMENT_FALLBACK_GAS_PRICE,
|
|
ZERO,
|
|
} from '../../src/core/constants';
|
|
import {
|
|
calculateDefaultFeeAmount,
|
|
calculatePriceImprovementAmount,
|
|
reviseQuoteWithFees,
|
|
FeeService,
|
|
} from '../../src/services/fee_service';
|
|
import { QuoteContext } from '../../src/services/types';
|
|
import { FeeWithDetails, IndicativeQuote, TokenMetadata } from '../../src/core/types';
|
|
import { ConfigManager } from '../../src/utils/config_manager';
|
|
import { GasStationAttendantEthereum } from '../../src/utils/GasStationAttendantEthereum';
|
|
import { TokenPriceOracle } from '../../src/utils/TokenPriceOracle';
|
|
import { AmmQuote, ZeroExApiClient } from '../../src/utils/ZeroExApiClient';
|
|
|
|
const feeTokenSymbol = 'fee';
|
|
const feeTokenAddress = 'feeTokenAddress';
|
|
const feeTokenDecimals = 18;
|
|
|
|
const buildFeeService = (overrides: {
|
|
feeModelConfiguration?: FeeModelConfiguration;
|
|
gasPrice?: BigNumber;
|
|
tradeTokenPrice?: BigNumber | null;
|
|
feeTokenPrice?: BigNumber | null;
|
|
ammQuote?: AmmQuote | null;
|
|
chainId?: number;
|
|
feeTokenMetadata?: TokenMetadata;
|
|
configManagerMock?: ConfigManager;
|
|
gasStationAttendantMock?: GasStationAttendantEthereum;
|
|
tokenPriceOracleMock?: TokenPriceOracle;
|
|
zeroExApiClientMock?: ZeroExApiClient;
|
|
}): FeeService => {
|
|
const chainId = overrides?.chainId || 1337;
|
|
const feeTokenMetadata = overrides?.feeTokenMetadata || {
|
|
symbol: feeTokenSymbol,
|
|
decimals: feeTokenDecimals,
|
|
tokenAddress: feeTokenAddress,
|
|
};
|
|
const feeModelConfiguration = overrides?.feeModelConfiguration || DEFAULT_FEE_MODEL_CONFIGURATION;
|
|
const gasPrice = overrides?.gasPrice || new BigNumber(1e9);
|
|
const tradeTokenPrice = overrides?.tradeTokenPrice || null;
|
|
const feeTokenPrice = overrides?.feeTokenPrice || null;
|
|
const ammQuote = overrides?.ammQuote || null;
|
|
|
|
const configManagerMock = mock(ConfigManager);
|
|
when(configManagerMock.getFeeModelConfiguration(chainId, anything(), anything())).thenReturn(feeModelConfiguration);
|
|
|
|
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
|
|
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(gasPrice);
|
|
|
|
const tokenPriceOracleMock = mock(TokenPriceOracle);
|
|
when(tokenPriceOracleMock.batchFetchTokenPriceAsync(anything())).thenResolve([tradeTokenPrice, feeTokenPrice]);
|
|
|
|
const zeroExApiClientMock = mock(ZeroExApiClient);
|
|
when(zeroExApiClientMock.fetchAmmQuoteAsync(anything())).thenResolve(ammQuote);
|
|
|
|
return new FeeService(
|
|
chainId,
|
|
feeTokenMetadata,
|
|
instance(overrides?.configManagerMock || configManagerMock),
|
|
instance(overrides?.gasStationAttendantMock || gasStationAttendantMock),
|
|
instance(overrides?.tokenPriceOracleMock || tokenPriceOracleMock),
|
|
instance(overrides?.zeroExApiClientMock || zeroExApiClientMock),
|
|
DEFAULT_MIN_EXPIRY_DURATION_MS,
|
|
);
|
|
};
|
|
|
|
describe('FeeService', () => {
|
|
const workflow = 'rfqm';
|
|
const txOrigin = 'registryAddress';
|
|
const makerToken = 'UsdcAddress';
|
|
const makerTokenDecimals = 6;
|
|
const makerTokenPrice = new BigNumber(1e-6);
|
|
|
|
const takerToken = 'WbtcAddress';
|
|
const takerTokenDecimals = 18;
|
|
const takerTokenPrice = new BigNumber(6e-14);
|
|
|
|
const gasPrice = new BigNumber(1e9);
|
|
const gasEstimate = RFQM_TX_OTC_ORDER_GAS_ESTIMATE;
|
|
|
|
const feeTokenPrice = new BigNumber(3e-15);
|
|
|
|
const integrator = {
|
|
apiKeys: [],
|
|
integratorId: 'integratorId',
|
|
allowedChainIds: [1, 3, 137, 1337],
|
|
label: 'dummy integrator',
|
|
rfqm: true,
|
|
rfqt: true,
|
|
};
|
|
|
|
const takerAddress = 'takerAddress';
|
|
|
|
afterAll(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
describe('calculateFeeAsync v0', () => {
|
|
const feeModelVersion = 0;
|
|
it('should calculate v0 fee for RFQm correctly', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(0.345e18);
|
|
const tradeSizeBps = 5;
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio: 0,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: takerTokenPrice,
|
|
feeTokenPrice,
|
|
});
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
kind: 'gasOnly',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: null,
|
|
feeTokenBaseUnitPriceUsd: null,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
it('should calculate v0 fee for RFQt correctly', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(0.345e18);
|
|
const tradeSizeBps = 5;
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio: 0,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: takerTokenPrice,
|
|
feeTokenPrice,
|
|
});
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow: 'rfqt',
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: new BigNumber(0),
|
|
details: {
|
|
kind: 'gasOnly',
|
|
feeModelVersion,
|
|
gasFeeAmount: new BigNumber(0),
|
|
gasPrice: new BigNumber(0),
|
|
},
|
|
breakdown: {},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: null,
|
|
feeTokenBaseUnitPriceUsd: null,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
});
|
|
|
|
describe('calculateFeeAsync v1', () => {
|
|
const feeModelVersion = 1;
|
|
it('should calculate v1 fee for RFQm selling correctly', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(0.345e18);
|
|
const tradeSizeBps = 5;
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio: 0,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: takerTokenPrice,
|
|
feeTokenPrice,
|
|
});
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
const expectedZeroExFeeAmount = assetFillAmount
|
|
.times(tradeSizeBps * BPS_TO_RATIO)
|
|
.times(takerTokenPrice)
|
|
.div(feeTokenPrice)
|
|
.integerValue();
|
|
const expectedTotalFeeAmount = expectedZeroExFeeAmount.plus(expectedGasFeeAmount);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedTotalFeeAmount,
|
|
details: {
|
|
kind: 'default',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: expectedZeroExFeeAmount,
|
|
tradeSizeBps,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: takerTokenPrice,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'volume',
|
|
tradeSizeBps,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: takerTokenPrice,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
it('should calculate v1 fee for RFQm buying correctly', async () => {
|
|
// Given
|
|
const isSelling = false;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(5000e6);
|
|
const tradeSizeBps = 4;
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio: 0,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice,
|
|
});
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
makerAmount: assetFillAmount,
|
|
isFirm: false,
|
|
takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
const expectedZeroExFeeAmount = assetFillAmount
|
|
.times(tradeSizeBps * BPS_TO_RATIO)
|
|
.times(makerTokenPrice)
|
|
.div(feeTokenPrice)
|
|
.integerValue();
|
|
const expectedTotalFeeAmount = expectedZeroExFeeAmount.plus(expectedGasFeeAmount);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedTotalFeeAmount,
|
|
details: {
|
|
kind: 'default',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: expectedZeroExFeeAmount,
|
|
tradeSizeBps,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'volume',
|
|
tradeSizeBps,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
it('should calculate v1 fee for RFQt selling correctly', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(0.345e18);
|
|
const tradeSizeBps = 5;
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio: 0,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: takerTokenPrice,
|
|
feeTokenPrice,
|
|
});
|
|
|
|
const rfqtFixedFee = RFQT_GAS_IMPROVEMENT_FALLBACK_GAS_PRICE.times(RFQT_GAS_IMPROVEMENT);
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow: 'rfqt',
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedZeroExFeeAmount = assetFillAmount
|
|
.times(tradeSizeBps * BPS_TO_RATIO)
|
|
.times(takerTokenPrice)
|
|
.div(feeTokenPrice)
|
|
.plus(rfqtFixedFee) // TODO: may need to delete this line if removing the RFQT gas improvement fee
|
|
.integerValue();
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'default',
|
|
feeModelVersion,
|
|
gasFeeAmount: new BigNumber(0),
|
|
gasPrice: new BigNumber(0),
|
|
zeroExFeeAmount: expectedZeroExFeeAmount,
|
|
tradeSizeBps,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: takerTokenPrice,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
breakdown: {
|
|
zeroEx: {
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'volume',
|
|
tradeSizeBps,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: takerTokenPrice,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
it('should not include zeroEx fee for non-configured pairs', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(0.345e18);
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
gasPrice,
|
|
tradeTokenPrice: takerTokenPrice,
|
|
feeTokenPrice,
|
|
});
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: false,
|
|
takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
kind: 'default',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: new BigNumber(0),
|
|
tradeSizeBps: 0,
|
|
feeTokenBaseUnitPriceUsd: null,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: new BigNumber(0),
|
|
details: {
|
|
kind: 'volume',
|
|
tradeSizeBps: 0,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: null,
|
|
feeTokenBaseUnitPriceUsd: null,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
it('should not include zeroEx fee if price oracle is down', async () => {
|
|
// Given
|
|
const isSelling = false;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(5000e6);
|
|
const tradeSizeBps = 4;
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio: 0,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice: null,
|
|
});
|
|
|
|
// When
|
|
const { feeWithDetails: fee } = await feeService.calculateFeeAsync({
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
makerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
});
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
kind: 'gasOnly',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: null,
|
|
feeTokenBaseUnitPriceUsd: null,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(fee).toMatchObject(expectedFee);
|
|
});
|
|
});
|
|
|
|
describe('calculateFeeAsync v2', () => {
|
|
const feeModelVersion = 2;
|
|
it('should calculate v2 `price improvement` based fee for sell correctly if price improvement detection succeeded', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(1e18);
|
|
const marginRakeRatio = 0.5;
|
|
|
|
const ammMakerAmount = new BigNumber(3450e6);
|
|
const expectedSlippage = new BigNumber(-0.01);
|
|
const estimatedAmmGasFeeWei = new BigNumber(100e9);
|
|
const decodedUniqueId = '1234-5678';
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: ammMakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expectedSlippage,
|
|
estimatedGasFeeWei: estimatedAmmGasFeeWei,
|
|
decodedUniqueId,
|
|
};
|
|
|
|
const mm1MakerAmount = new BigNumber(3550e6);
|
|
const mm2MakerAmount = new BigNumber(3600e6);
|
|
const mmQuotes: IndicativeQuote[] = [
|
|
{
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm1MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
{
|
|
maker: 'maker2Address',
|
|
makerUri: 'http://maker2.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm2MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
];
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio,
|
|
tradeSizeBps: 0,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice,
|
|
ammQuote,
|
|
});
|
|
|
|
const quoteContext: QuoteContext = {
|
|
workflow,
|
|
chainId: 1337,
|
|
isFirm: true,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
};
|
|
|
|
// When
|
|
jest.useFakeTimers().setSystemTime(1650000000000);
|
|
const { feeWithDetails, quotesWithGasFee, ammQuoteUniqueId } = await feeService.calculateFeeAsync(
|
|
quoteContext,
|
|
async () => {
|
|
return Promise.resolve(mmQuotes);
|
|
},
|
|
);
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
const expectedMargin = mm2MakerAmount
|
|
.minus(ammMakerAmount.times(new BigNumber(1).plus(expectedSlippage)))
|
|
.times(makerTokenPrice)
|
|
.div(feeTokenPrice)
|
|
.plus(estimatedAmmGasFeeWei)
|
|
.integerValue();
|
|
const expectedZeroExFeeAmount = expectedMargin.times(marginRakeRatio).integerValue();
|
|
const expectedTotalFeeAmount = expectedZeroExFeeAmount.plus(expectedGasFeeAmount);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedTotalFeeAmount,
|
|
details: {
|
|
kind: 'margin',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: expectedZeroExFeeAmount,
|
|
margin: expectedMargin,
|
|
marginRakeRatio,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'price_improvement',
|
|
priceImprovement: expectedMargin,
|
|
rakeRatio: marginRakeRatio,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
};
|
|
expect(feeWithDetails).toMatchObject(expectedFee);
|
|
expect(quotesWithGasFee).toMatchObject(mmQuotes);
|
|
expect(ammQuoteUniqueId).toBe(decodedUniqueId);
|
|
|
|
// When
|
|
const revisedQuotes = await feeService.reviseQuotesAsync(
|
|
// $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
quotesWithGasFee!,
|
|
expectedZeroExFeeAmount,
|
|
quoteContext,
|
|
);
|
|
|
|
// Then
|
|
const expectedRevisedQuotes = mmQuotes.map((quote) =>
|
|
reviseQuoteWithFees(quote, expectedZeroExFeeAmount, isSelling, makerTokenPrice, feeTokenPrice),
|
|
);
|
|
|
|
expect(revisedQuotes).toMatchObject(expectedRevisedQuotes);
|
|
});
|
|
it('should calculate v2 `price improvement` based fee for buy correctly if price improvement detection succeeded', async () => {
|
|
// Given
|
|
const isSelling = false;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(1e18);
|
|
const marginRakeRatio = 0.4;
|
|
|
|
const ammTakerAmount = new BigNumber(3450e6);
|
|
const expectedSlippage = new BigNumber(-0.1);
|
|
const estimatedAmmGasFeeWei = new BigNumber(100e9);
|
|
const decodedUniqueId = '1234-5678';
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: ammTakerAmount,
|
|
expectedSlippage,
|
|
estimatedGasFeeWei: estimatedAmmGasFeeWei,
|
|
decodedUniqueId,
|
|
};
|
|
|
|
const mm1TakerAmount = new BigNumber(3400e6);
|
|
const mm2TakerAmount = new BigNumber(3350e6);
|
|
const mmQuotes: IndicativeQuote[] = [
|
|
{
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: mm1TakerAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
{
|
|
maker: 'maker2Address',
|
|
makerUri: 'http://maker2.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: mm2TakerAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
];
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio,
|
|
tradeSizeBps: 0,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: takerTokenPrice,
|
|
feeTokenPrice,
|
|
ammQuote,
|
|
});
|
|
|
|
const quoteContext: QuoteContext = {
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
makerAmount: assetFillAmount,
|
|
isFirm: false,
|
|
takerAddress,
|
|
integrator,
|
|
};
|
|
|
|
// When
|
|
jest.useFakeTimers().setSystemTime(1650000000000);
|
|
const { feeWithDetails, quotesWithGasFee, ammQuoteUniqueId } = await feeService.calculateFeeAsync(
|
|
quoteContext,
|
|
async () => {
|
|
return Promise.resolve(mmQuotes);
|
|
},
|
|
);
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
const expectedMargin = ammTakerAmount
|
|
.times(new BigNumber(1).minus(expectedSlippage))
|
|
.minus(mm2TakerAmount)
|
|
.times(takerTokenPrice)
|
|
.div(feeTokenPrice)
|
|
.plus(estimatedAmmGasFeeWei)
|
|
.integerValue();
|
|
const expectedZeroExFeeAmount = expectedMargin.times(marginRakeRatio).integerValue();
|
|
const expectedTotalFeeAmount = expectedZeroExFeeAmount.plus(expectedGasFeeAmount);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedTotalFeeAmount,
|
|
details: {
|
|
kind: 'margin',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: expectedZeroExFeeAmount,
|
|
margin: expectedMargin,
|
|
marginRakeRatio,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: takerTokenPrice,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'price_improvement',
|
|
priceImprovement: expectedMargin,
|
|
rakeRatio: marginRakeRatio,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: takerTokenPrice,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(feeWithDetails).toMatchObject(expectedFee);
|
|
expect(quotesWithGasFee).toMatchObject(mmQuotes);
|
|
expect(ammQuoteUniqueId).toBe(decodedUniqueId);
|
|
|
|
// When
|
|
const revisedQuotes = await feeService.reviseQuotesAsync(
|
|
// $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
quotesWithGasFee!,
|
|
expectedZeroExFeeAmount,
|
|
quoteContext,
|
|
);
|
|
|
|
// Then
|
|
const expectedRevisedQuotes = mmQuotes.map((quote) =>
|
|
reviseQuoteWithFees(quote, expectedZeroExFeeAmount, isSelling, takerTokenPrice, feeTokenPrice),
|
|
);
|
|
|
|
expect(revisedQuotes).toMatchObject(expectedRevisedQuotes);
|
|
});
|
|
it('should calculate v2 `default` fee correctly if token price query succeeded but 0x-api query failed', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(1e18);
|
|
const marginRakeRatio = 0.5;
|
|
const tradeSizeBps = 5;
|
|
|
|
const ammQuote = null;
|
|
|
|
const mm1MakerAmount = new BigNumber(3550e6);
|
|
const mm2MakerAmount = new BigNumber(3600e6);
|
|
const mmQuotes: IndicativeQuote[] = [
|
|
{
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm1MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
{
|
|
maker: 'maker2Address',
|
|
makerUri: 'http://maker2.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm2MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
];
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice,
|
|
ammQuote,
|
|
});
|
|
|
|
const quoteContext: QuoteContext = {
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
};
|
|
|
|
// When
|
|
jest.useFakeTimers().setSystemTime(1650000000000);
|
|
const { feeWithDetails, quotesWithGasFee, ammQuoteUniqueId } = await feeService.calculateFeeAsync(
|
|
quoteContext,
|
|
async () => {
|
|
return Promise.resolve(mmQuotes);
|
|
},
|
|
);
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
const expectedZeroExFeeAmount = calculateDefaultFeeAmount(
|
|
mm2MakerAmount,
|
|
tradeSizeBps,
|
|
makerTokenPrice,
|
|
feeTokenPrice,
|
|
);
|
|
const expectedTotalFeeAmount = expectedZeroExFeeAmount.plus(expectedGasFeeAmount);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedTotalFeeAmount,
|
|
details: {
|
|
kind: 'default',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: expectedZeroExFeeAmount,
|
|
tradeSizeBps,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: expectedZeroExFeeAmount,
|
|
details: {
|
|
kind: 'volume',
|
|
tradeSizeBps,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
};
|
|
expect(feeWithDetails).toMatchObject(expectedFee);
|
|
expect(quotesWithGasFee).toMatchObject(mmQuotes);
|
|
expect(ammQuoteUniqueId).toBe(undefined);
|
|
|
|
// When
|
|
const revisedQuotes = await feeService.reviseQuotesAsync(
|
|
// $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
quotesWithGasFee!,
|
|
expectedZeroExFeeAmount,
|
|
quoteContext,
|
|
);
|
|
|
|
// Then
|
|
const expectedRevisedQuotes = mmQuotes.map((quote) =>
|
|
reviseQuoteWithFees(quote, expectedZeroExFeeAmount, isSelling, makerTokenPrice, feeTokenPrice),
|
|
);
|
|
|
|
expect(revisedQuotes).toMatchObject(expectedRevisedQuotes);
|
|
});
|
|
it('should calculate v2 `gasOnly` fee correctly if token price query failed', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(1e18);
|
|
const marginRakeRatio = 0.5;
|
|
const tradeSizeBps = 5;
|
|
|
|
const ammMakerAmount = new BigNumber(3450e6);
|
|
const expectedSlippage = new BigNumber(-0.01);
|
|
const estimatedAmmGasFeeWei = new BigNumber(100e9);
|
|
const decodedUniqueId = '1234-5678';
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: ammMakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expectedSlippage,
|
|
estimatedGasFeeWei: estimatedAmmGasFeeWei,
|
|
decodedUniqueId,
|
|
};
|
|
|
|
const mm1MakerAmount = new BigNumber(3550e6);
|
|
const mm2MakerAmount = new BigNumber(3600e6);
|
|
const mmQuotes: IndicativeQuote[] = [
|
|
{
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm1MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
{
|
|
maker: 'maker2Address',
|
|
makerUri: 'http://maker2.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm2MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
];
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio,
|
|
tradeSizeBps,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice: null,
|
|
ammQuote,
|
|
});
|
|
|
|
// When
|
|
jest.useFakeTimers().setSystemTime(1650000000000);
|
|
const { feeWithDetails, quotesWithGasFee, ammQuoteUniqueId } = await feeService.calculateFeeAsync(
|
|
{
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
},
|
|
async () => {
|
|
return Promise.resolve(mmQuotes);
|
|
},
|
|
);
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
kind: 'gasOnly',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: null,
|
|
feeTokenBaseUnitPriceUsd: null,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: null,
|
|
},
|
|
};
|
|
expect(feeWithDetails).toMatchObject(expectedFee);
|
|
expect(quotesWithGasFee).toMatchObject(mmQuotes);
|
|
expect(ammQuoteUniqueId).toBe(decodedUniqueId);
|
|
});
|
|
it('should calculate v2 `price improvement` based fee with zero zeroExFee if price improvement is zero', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(1e18);
|
|
const marginRakeRatio = 0.5;
|
|
|
|
const ammMakerAmount = new BigNumber(4000e6);
|
|
const expectedSlippage = new BigNumber(-0.01);
|
|
const estimatedAmmGasFeeWei = new BigNumber(100e9);
|
|
const decodedUniqueId = '1234-5678';
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: ammMakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expectedSlippage,
|
|
estimatedGasFeeWei: estimatedAmmGasFeeWei,
|
|
decodedUniqueId,
|
|
};
|
|
|
|
const mm1MakerAmount = new BigNumber(3550e6);
|
|
const mm2MakerAmount = new BigNumber(3600e6);
|
|
const mmQuotes: IndicativeQuote[] = [
|
|
{
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm1MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
{
|
|
maker: 'maker2Address',
|
|
makerUri: 'http://maker2.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: mm2MakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
},
|
|
];
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio,
|
|
tradeSizeBps: 0,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice,
|
|
ammQuote,
|
|
});
|
|
|
|
// When
|
|
jest.useFakeTimers().setSystemTime(1650000000000);
|
|
const { feeWithDetails, quotesWithGasFee, ammQuoteUniqueId } = await feeService.calculateFeeAsync(
|
|
{
|
|
workflow,
|
|
chainId: 1337,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
isFirm: true,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
},
|
|
async () => {
|
|
return Promise.resolve(mmQuotes);
|
|
},
|
|
);
|
|
|
|
// Then
|
|
const expectedGasFeeAmount = gasPrice.times(gasEstimate);
|
|
|
|
const expectedFee: FeeWithDetails = {
|
|
type: 'fixed',
|
|
token: feeTokenAddress,
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
kind: 'margin',
|
|
feeModelVersion,
|
|
gasFeeAmount: expectedGasFeeAmount,
|
|
gasPrice,
|
|
zeroExFeeAmount: ZERO,
|
|
margin: ZERO,
|
|
marginRakeRatio,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: expectedGasFeeAmount,
|
|
details: {
|
|
gasPrice,
|
|
estimatedGas: new BigNumber(gasEstimate),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: ZERO,
|
|
details: {
|
|
kind: 'price_improvement',
|
|
priceImprovement: ZERO,
|
|
rakeRatio: marginRakeRatio,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
feeTokenBaseUnitPriceUsd: feeTokenPrice,
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: makerTokenPrice,
|
|
},
|
|
};
|
|
expect(feeWithDetails).toMatchObject(expectedFee);
|
|
expect(quotesWithGasFee).toMatchObject(mmQuotes);
|
|
expect(ammQuoteUniqueId).toBe(decodedUniqueId);
|
|
});
|
|
it('should throw if called from RFQt workflow', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const isUnwrap = false;
|
|
const assetFillAmount = new BigNumber(1e18);
|
|
const marginRakeRatio = 0.5;
|
|
|
|
const ammMakerAmount = new BigNumber(3450e6);
|
|
const expectedSlippage = new BigNumber(-0.01);
|
|
const estimatedAmmGasFeeWei = new BigNumber(100e9);
|
|
const decodedUniqueId = '1234-5678';
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: ammMakerAmount,
|
|
takerAmount: assetFillAmount,
|
|
expectedSlippage,
|
|
estimatedGasFeeWei: estimatedAmmGasFeeWei,
|
|
decodedUniqueId,
|
|
};
|
|
|
|
const feeService: FeeService = buildFeeService({
|
|
feeModelConfiguration: {
|
|
marginRakeRatio,
|
|
tradeSizeBps: 0,
|
|
},
|
|
gasPrice,
|
|
tradeTokenPrice: makerTokenPrice,
|
|
feeTokenPrice,
|
|
ammQuote,
|
|
});
|
|
|
|
const quoteContext: QuoteContext = {
|
|
workflow: 'rfqt',
|
|
chainId: 1337,
|
|
isFirm: true,
|
|
feeModelVersion,
|
|
txOrigin,
|
|
makerToken,
|
|
takerToken,
|
|
originalMakerToken: makerToken,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
isUnwrap,
|
|
isSelling,
|
|
assetFillAmount,
|
|
takerAmount: assetFillAmount,
|
|
takerAddress,
|
|
trader: takerAddress,
|
|
integrator,
|
|
};
|
|
|
|
// When
|
|
await expect(() => feeService.calculateFeeAsync(quoteContext)).rejects.toThrow('Not implemented');
|
|
});
|
|
});
|
|
|
|
describe('pure function calculateDefaultFeeAmount()', () => {
|
|
it('should calculate default fee amount correctly', async () => {
|
|
// Given
|
|
const tradeTokenAmount = new BigNumber(1e18);
|
|
const feeRateBps = 5;
|
|
const tradeTokenBaseUnitPriceUsd = new BigNumber(6e-14);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const defaultFeeAmount = calculateDefaultFeeAmount(
|
|
tradeTokenAmount,
|
|
feeRateBps,
|
|
tradeTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
const expectedDefaultFeeAmount = new BigNumber(1e16);
|
|
expect(defaultFeeAmount).toMatchObject(expectedDefaultFeeAmount);
|
|
});
|
|
it('should return zero if bps is zero', async () => {
|
|
// Given
|
|
const tradeTokenAmount = new BigNumber(1e18);
|
|
const feeRateBps = 0;
|
|
const tradeTokenBaseUnitPriceUsd = new BigNumber(6e-14);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const defaultFeeAmount = calculateDefaultFeeAmount(
|
|
tradeTokenAmount,
|
|
feeRateBps,
|
|
tradeTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
expect(defaultFeeAmount).toMatchObject(ZERO);
|
|
});
|
|
it('should return zero if either trade token price or fee token price is null', async () => {
|
|
// Given
|
|
const tradeTokenAmount = new BigNumber(1e18);
|
|
const feeRateBps = 5;
|
|
const tradeTokenBaseUnitPriceUsd = new BigNumber(6e-14);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const defaultFeeAmount1 = calculateDefaultFeeAmount(
|
|
tradeTokenAmount,
|
|
feeRateBps,
|
|
null,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
const defaultFeeAmount2 = calculateDefaultFeeAmount(
|
|
tradeTokenAmount,
|
|
feeRateBps,
|
|
tradeTokenBaseUnitPriceUsd,
|
|
null,
|
|
);
|
|
|
|
// Then
|
|
expect(defaultFeeAmount1).toMatchObject(ZERO);
|
|
expect(defaultFeeAmount2).toMatchObject(ZERO);
|
|
});
|
|
});
|
|
describe('pure function calculatePriceImprovementAmount()', () => {
|
|
it('should calculate price improvement amount for selling correctly', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const assetFillAmount = new BigNumber(3e17);
|
|
const makerQuoteWithGasFee: IndicativeQuote = {
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: new BigNumber(1100e6),
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
};
|
|
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: new BigNumber(1000e6),
|
|
takerAmount: assetFillAmount,
|
|
expectedSlippage: new BigNumber(-0.02),
|
|
estimatedGasFeeWei: new BigNumber(10e15),
|
|
};
|
|
|
|
const quoteTokenBaseUnitPriceUsd = new BigNumber(1e-6);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const priceImprovementAmount = calculatePriceImprovementAmount(
|
|
makerQuoteWithGasFee,
|
|
ammQuote,
|
|
isSelling,
|
|
quoteTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
const expectedPriceImprovementAmount = new BigNumber(50e15);
|
|
expect(priceImprovementAmount).toMatchObject(expectedPriceImprovementAmount);
|
|
});
|
|
it('should calculate price improvement amount for buying correctly', async () => {
|
|
// Given
|
|
const isSelling = false;
|
|
const assetFillAmount = new BigNumber(3e17);
|
|
const makerQuoteWithGasFee: IndicativeQuote = {
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: new BigNumber(900e6),
|
|
expiry: new BigNumber(1652722767),
|
|
};
|
|
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: new BigNumber(1000e6),
|
|
expectedSlippage: new BigNumber(-0.02),
|
|
estimatedGasFeeWei: new BigNumber(10e15),
|
|
};
|
|
|
|
const quoteTokenBaseUnitPriceUsd = new BigNumber(1e-6);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const priceImprovementAmount = calculatePriceImprovementAmount(
|
|
makerQuoteWithGasFee,
|
|
ammQuote,
|
|
isSelling,
|
|
quoteTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
const expectedPriceImprovementAmount = new BigNumber(50e15);
|
|
expect(priceImprovementAmount).toMatchObject(expectedPriceImprovementAmount);
|
|
});
|
|
it('should return zero if there is no price improvement', async () => {
|
|
// Given
|
|
const isSelling = false;
|
|
const assetFillAmount = new BigNumber(3e17);
|
|
const makerQuoteWithGasFee: IndicativeQuote = {
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: new BigNumber(1051e6),
|
|
expiry: new BigNumber(1652722767),
|
|
};
|
|
|
|
const ammQuote: AmmQuote = {
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: new BigNumber(1000e6),
|
|
expectedSlippage: new BigNumber(-0.02),
|
|
estimatedGasFeeWei: new BigNumber(10e15),
|
|
};
|
|
|
|
const quoteTokenBaseUnitPriceUsd = new BigNumber(1e-6);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const priceImprovementAmount = calculatePriceImprovementAmount(
|
|
makerQuoteWithGasFee,
|
|
ammQuote,
|
|
isSelling,
|
|
quoteTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
expect(priceImprovementAmount).toMatchObject(ZERO);
|
|
});
|
|
});
|
|
describe('pure function reviseQuoteWithZeroExFee()', () => {
|
|
it('should revise quote correctly for selling', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const assetFillAmount = new BigNumber(3e17);
|
|
const makerQuoteWithGasFee: IndicativeQuote = {
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: new BigNumber(1000e6),
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
};
|
|
|
|
const zeroExFeeAmount = new BigNumber(10e15);
|
|
|
|
const quoteTokenBaseUnitPriceUsd = new BigNumber(1e-6);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const revisedQuote = reviseQuoteWithFees(
|
|
makerQuoteWithGasFee,
|
|
zeroExFeeAmount,
|
|
isSelling,
|
|
quoteTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
const expectedRevisedMakerAmount = new BigNumber(970e6);
|
|
expect(revisedQuote.makerAmount).toMatchObject(expectedRevisedMakerAmount);
|
|
});
|
|
it('should revise quote correctly for buying', async () => {
|
|
// Given
|
|
const isSelling = false;
|
|
const assetFillAmount = new BigNumber(3e17);
|
|
const makerQuoteWithGasFee: IndicativeQuote = {
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: assetFillAmount,
|
|
takerAmount: new BigNumber(1000e6),
|
|
expiry: new BigNumber(1652722767),
|
|
};
|
|
|
|
const zeroExFeeAmount = new BigNumber(10e15);
|
|
|
|
const quoteTokenBaseUnitPriceUsd = new BigNumber(1e-6);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const revisedQuote = reviseQuoteWithFees(
|
|
makerQuoteWithGasFee,
|
|
zeroExFeeAmount,
|
|
isSelling,
|
|
quoteTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
const expectedRevisedTakerAmount = new BigNumber(1030e6);
|
|
expect(revisedQuote.takerAmount).toMatchObject(expectedRevisedTakerAmount);
|
|
});
|
|
it('should not revise quote correctly for zero zeroExFee', async () => {
|
|
// Given
|
|
const isSelling = true;
|
|
const assetFillAmount = new BigNumber(3e17);
|
|
const makerQuoteWithGasFee: IndicativeQuote = {
|
|
maker: 'maker1Address',
|
|
makerUri: 'http://maker1.com',
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: new BigNumber(1000e6),
|
|
takerAmount: assetFillAmount,
|
|
expiry: new BigNumber(1652722767),
|
|
};
|
|
|
|
const zeroExFeeAmount = ZERO;
|
|
|
|
const quoteTokenBaseUnitPriceUsd = new BigNumber(1e-6);
|
|
const feeTokenBaseUnitPriceUsd = new BigNumber(3e-15);
|
|
|
|
// When
|
|
const revisedQuote = reviseQuoteWithFees(
|
|
makerQuoteWithGasFee,
|
|
zeroExFeeAmount,
|
|
isSelling,
|
|
quoteTokenBaseUnitPriceUsd,
|
|
feeTokenBaseUnitPriceUsd,
|
|
);
|
|
|
|
// Then
|
|
const expectedRevisedMakerAmount = new BigNumber(1000e6);
|
|
expect(revisedQuote.makerAmount).toMatchObject(expectedRevisedMakerAmount);
|
|
expect(revisedQuote.takerAmount).toMatchObject(assetFillAmount);
|
|
});
|
|
});
|
|
});
|