Files
protocol/apps-node/rfq-api/test/utils/quote_server_client_test.ts

677 lines
25 KiB
TypeScript

import { ethSignHashWithKey, OtcOrder } from '@0x/protocol-utils';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import Axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import * as HttpStatus from 'http-status-codes';
import { Integrator } from '../../src/config';
import { SignRequest } from '../../src/quote-server/types';
import { Fee, QuoteServerPriceParams } from '../../src/core/types';
import { QuoteServerClient } from '../../src/utils/quote_server_client';
import { CHAIN_ID, CONTRACT_ADDRESSES } from '../constants';
import { MarketOperation } from '@0x/types';
const makerUri = 'https://some-market-maker.xyz';
const integrator: Integrator = {
integratorId: 'some-integrator-id',
apiKeys: [],
allowedChainIds: [],
label: 'integrator',
rfqm: true,
};
// Maker
const makerAddress = '0xFDbEf5C1Ad7d173D191D565c14E28eBd5b50470e';
const makerPrivateKey = '0xf4559ca5152145f5e0b9762f12213c2e74020a4481953fb940413273051a89d3';
// Taker
const takerAddress = '0xdA9AC423442169588DE6b4305f4E820D708d0cE5';
const takerPrivateKey = '0x653fa328df81be180b58e42737bc4cef037a19a3b9673b15d20ee2eebb2e509d';
// Some tokens and amounts
const takerToken = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const makerToken = '0x6b175474e89094c44da98b954eedeac495271d0f';
const takerAmount = new BigNumber(100);
const makerAmount = new BigNumber(100_000);
// An OtcOrder
const order = new OtcOrder({
maker: makerAddress,
taker: takerAddress,
makerAmount,
takerAmount,
makerToken,
takerToken,
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(2634330177),
new BigNumber(1),
new BigNumber(1634330177),
),
});
const orderHash = order.getHash();
// Signatures
const takerSignature = ethSignHashWithKey(orderHash, takerPrivateKey);
const makerSignature = ethSignHashWithKey(orderHash, makerPrivateKey);
describe('QuoteServerClient', () => {
const axiosInstance = Axios.create();
const axiosMock = new AxiosMockAdapter(axiosInstance);
afterEach(() => {
axiosMock.reset();
});
describe('makeQueryParameters', () => {
it('should make RFQt request parameters', () => {
// Given
const marketOperation = MarketOperation.Sell;
const input = {
txOrigin: takerAddress,
takerAddress: NULL_ADDRESS,
marketOperation,
buyTokenAddress: makerToken,
sellTokenAddress: takerToken,
comparisonPrice: new BigNumber('42'),
assetFillAmount: new BigNumber('100000'),
};
// When
const params = QuoteServerClient.makeQueryParameters(input);
// Then
expect(params).toEqual({
buyTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
protocolVersion: '4',
sellAmountBaseUnits: '100000',
sellTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
takerAddress: '0x0000000000000000000000000000000000000000',
txOrigin: '0xdA9AC423442169588DE6b4305f4E820D708d0cE5',
comparisonPrice: '42',
});
});
it('should make RFQm request parameters', () => {
// Given
const txOrigin = '0x335e51687677C4f1389f3dEcA259af983529e82D';
const feeAmount = '100';
const otcOrderFee: Fee = {
amount: new BigNumber(feeAmount),
token: CONTRACT_ADDRESSES.etherToken,
type: 'fixed',
};
const marketOperation = MarketOperation.Sell;
// When
const params = QuoteServerClient.makeQueryParameters({
txOrigin,
takerAddress,
marketOperation,
buyTokenAddress: makerToken,
sellTokenAddress: takerToken,
assetFillAmount: new BigNumber('100000'),
isLastLook: true,
fee: otcOrderFee,
});
// Then
expect(params).toEqual({
buyTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
feeAmount: '100',
feeToken: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082',
feeType: 'fixed',
isLastLook: 'true',
protocolVersion: '4',
sellAmountBaseUnits: '100000',
sellTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
takerAddress: '0xdA9AC423442169588DE6b4305f4E820D708d0cE5',
txOrigin: '0x335e51687677C4f1389f3dEcA259af983529e82D',
});
});
it('should make RFQm request parameters with chain id', () => {
// Given
const txOrigin = '0x335e51687677C4f1389f3dEcA259af983529e82D';
const feeAmount = '100';
const otcOrderFee: Fee = {
amount: new BigNumber(feeAmount),
token: CONTRACT_ADDRESSES.etherToken,
type: 'fixed',
};
const marketOperation = MarketOperation.Sell;
// When
const params = QuoteServerClient.makeQueryParameters({
chainId: 10,
txOrigin,
takerAddress,
marketOperation,
buyTokenAddress: makerToken,
sellTokenAddress: takerToken,
assetFillAmount: new BigNumber('100000'),
isLastLook: true,
fee: otcOrderFee,
});
// Then
expect(params).toEqual({
chainId: '10',
buyTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
feeAmount: '100',
feeToken: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082',
feeType: 'fixed',
isLastLook: 'true',
protocolVersion: '4',
sellAmountBaseUnits: '100000',
sellTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
takerAddress: '0xdA9AC423442169588DE6b4305f4E820D708d0cE5',
txOrigin: '0x335e51687677C4f1389f3dEcA259af983529e82D',
});
});
});
describe('OtcOrder', () => {
describe('getPriceV2Async', () => {
it('should return a valid indicative quote', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: QuoteServerPriceParams = {
chainId: CHAIN_ID.toString(),
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
takerAddress,
sellAmountBaseUnits: takerAmount.toString(),
protocolVersion: '4',
txOrigin: takerAddress,
isLastLook: 'true',
feeAmount: '100',
feeType: 'fixed',
feeToken: CONTRACT_ADDRESSES.etherToken,
nonce: '1634322835',
nonceBucket: '1',
};
const response = {
maker: makerAddress,
makerAmount,
takerAmount,
makerToken,
takerToken,
expiry: new BigNumber(9934322972),
};
axiosMock.onGet(`${makerUri}/rfqm/v2/price`).replyOnce(HttpStatus.OK, response);
// When
const indicativeQuote = await client.getPriceV2Async(
makerUri,
integrator,
request,
(uri) => `${uri}/rfqm/v2/price`,
);
// Then
const expectedResponse = {
...response,
makerUri,
};
expect(indicativeQuote).toEqual(expectedResponse);
});
it('should return undefined for empty responses (not quoting)', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: QuoteServerPriceParams = {
chainId: CHAIN_ID.toString(),
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
takerAddress,
sellAmountBaseUnits: takerAmount.toString(),
protocolVersion: '4',
txOrigin: takerAddress,
isLastLook: 'true',
feeAmount: '100',
feeType: 'fixed',
feeToken: CONTRACT_ADDRESSES.etherToken,
nonce: '1634322835',
nonceBucket: '1',
};
const response = {}; // empty response
// When
axiosMock.onGet(`${makerUri}/rfqm/v2/price`).replyOnce(HttpStatus.OK, response);
const result = await client.getPriceV2Async(
makerUri,
integrator,
request,
(uri) => `${uri}/rfqm/v2/price`,
);
// Then
expect(result).toBe(undefined);
});
it('should return undefined when returning a 400 from axios', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: QuoteServerPriceParams = {
chainId: CHAIN_ID.toString(),
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
takerAddress,
sellAmountBaseUnits: takerAmount.toString(),
protocolVersion: '4',
txOrigin: takerAddress,
isLastLook: 'true',
feeAmount: '100',
feeType: 'fixed',
feeToken: CONTRACT_ADDRESSES.etherToken,
nonce: '1634322835',
nonceBucket: '1',
};
const response = {}; // empty response
axiosMock.onGet(`${makerUri}/rfqm/v2/price`).replyOnce(HttpStatus.BAD_REQUEST, response);
// When
const result = await client.getPriceV2Async(
makerUri,
integrator,
request,
(uri) => `${uri}/rfqm/v2/price`,
);
// Then
expect(result).toBe(undefined);
});
it('should throw an error for a malformed response', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: QuoteServerPriceParams = {
chainId: CHAIN_ID.toString(),
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
takerAddress,
sellAmountBaseUnits: takerAmount.toString(),
protocolVersion: '4',
txOrigin: takerAddress,
isLastLook: 'true',
feeAmount: '100',
feeType: 'fixed',
feeToken: CONTRACT_ADDRESSES.etherToken,
nonce: '1634322835',
nonceBucket: '1',
};
const response = {
asdf: 'malformed response',
};
axiosMock.onGet(`${makerUri}/rfqm/v2/price`).replyOnce(HttpStatus.OK, response);
// When
await expect(async () => {
await client.getPriceV2Async(makerUri, integrator, request, (uri) => `${uri}/rfqm/v2/price`);
}).rejects.toThrow();
});
});
describe('batchGetPriceV2Async', () => {
it('should return the valid indicative qutoes and filter out errors', async () => {
// Given
const makerUri1 = 'https://some-market-maker1.xyz';
const makerUri2 = 'https://some-market-maker2.xyz';
const makerUri3 = 'https://some-market-maker3.xyz';
const client = new QuoteServerClient(axiosInstance);
const request: QuoteServerPriceParams = {
chainId: CHAIN_ID.toString(),
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
takerAddress,
sellAmountBaseUnits: takerAmount.toString(),
protocolVersion: '4',
txOrigin: takerAddress,
isLastLook: 'true',
feeAmount: '100',
feeType: 'fixed',
feeToken: CONTRACT_ADDRESSES.etherToken,
nonce: '1634322835',
nonceBucket: '1',
};
const response = {
maker: makerAddress,
makerAmount,
takerAmount,
makerToken,
takerToken,
expiry: new BigNumber(9934322972),
};
axiosMock.onGet(`${makerUri1}/rfqm/v2/price`).replyOnce(HttpStatus.OK, response);
axiosMock.onGet(`${makerUri2}/rfqm/v2/price`).replyOnce(HttpStatus.NO_CONTENT, {});
axiosMock.onGet(`${makerUri3}/rfqm/v2/price`).replyOnce(HttpStatus.BAD_GATEWAY, {});
// When
const indicativeQuotes = await client.batchGetPriceV2Async(
[makerUri1, makerUri2, makerUri3],
integrator,
request,
);
// Then
// $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(indicativeQuotes!.length).toEqual(1);
expect(indicativeQuotes[0].makerAmount.toNumber()).toEqual(response.makerAmount.toNumber());
expect(indicativeQuotes[0].maker).toEqual(makerAddress);
});
});
describe('signV2Async', () => {
it('should return a signature for valid response', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('100'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const actualRequest = {
order,
orderHash,
feeAmount: '100',
feeToken: CONTRACT_ADDRESSES.etherToken,
expiry: order.expiry,
takerSignature,
// trader: takerAddress,
// workflow: 'rfqm',
};
const response = {
feeAmount: '100',
proceedWithFill: true,
makerSignature,
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(actualRequest)))
.replyOnce(HttpStatus.OK, response);
// When
const signature = await client.signV2Async(makerUri, 'dummy-integrator-id', request);
// Then
expect(signature).toEqual(makerSignature);
});
it('should send takerSpecifiedSide when enabled and present in request', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('100'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSpecifiedSide: 'makerToken',
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const actualRequest = {
order,
orderHash,
feeAmount: '100',
feeToken: CONTRACT_ADDRESSES.etherToken,
expiry: order.expiry,
takerSpecifiedSide: 'makerToken',
takerSignature,
// trader: takerAddress,
// workflow: 'rfqm',
};
const response = {
feeAmount: '100',
proceedWithFill: true,
makerSignature,
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(actualRequest)))
.replyOnce(HttpStatus.OK, response);
// When
const signature = await client.signV2Async(makerUri, 'dummy-integrator-id', request);
// Then
expect(signature).toEqual(makerSignature);
});
it('should return a signature for valid response even if the fee is higher than requested', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('100'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const actualRequest = {
order,
orderHash,
feeAmount: '100',
feeToken: CONTRACT_ADDRESSES.etherToken,
expiry: order.expiry,
takerSignature,
// trader: takerAddress,
// workflow: 'rfqm',
};
const response = {
feeAmount: '101', // higher than requested
proceedWithFill: true,
makerSignature,
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(actualRequest)))
.replyOnce(HttpStatus.OK, response);
// When
const signature = await client.signV2Async(makerUri, 'dummy-integrator-id', request);
// Then
expect(signature).toEqual(makerSignature);
});
it('should return a signature for valid response if the fee is 0 but no acknowledgement is returned', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('0'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const actualRequest = {
order,
orderHash,
feeAmount: '0',
feeToken: CONTRACT_ADDRESSES.etherToken,
expiry: order.expiry,
takerSignature,
// trader: takerAddress,
// workflow: 'rfqm',
};
const response = {
feeAmount: undefined, // no fee amount acknowledged
proceedWithFill: true,
makerSignature,
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(actualRequest)))
.replyOnce(HttpStatus.OK, response);
// When
const signature = await client.signV2Async(makerUri, 'dummy-integrator-id', request);
// Then
expect(signature).toEqual(makerSignature);
});
it('should throw an error for a malformed response', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('100'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const response = {
asdf: 'I am broken',
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(request)))
.replyOnce(HttpStatus.OK, response);
await expect(async () => {
await client.signV2Async(makerUri, 'dummy-integrator-id', request);
}).rejects.toThrow();
});
it('should return undefined for an incorrect fee acknowledgement', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('100'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const actualRequest = {
order,
orderHash,
feeAmount: '100',
feeToken: CONTRACT_ADDRESSES.etherToken,
expiry: order.expiry,
takerSignature,
// trader: takerAddress,
// workflow: 'rfqm',
};
const response = {
feeAmount: '10', // Not the right fee
proceedWithFill: true,
makerSignature,
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(actualRequest)))
.replyOnce(HttpStatus.OK, response);
// When
const signature = await client.signV2Async(makerUri, 'dummy-integrator-id', request);
// Then
expect(signature).toBe(undefined);
});
it('should return undefined for explicitly rejected responses', async () => {
// Given
const client = new QuoteServerClient(axiosInstance);
const request: SignRequest = {
order,
orderHash,
fee: {
amount: new BigNumber('100'),
type: 'fixed',
token: CONTRACT_ADDRESSES.etherToken,
},
expiry: order.expiry,
takerSignature,
trader: takerAddress,
workflow: 'rfqm',
};
const actualRequest = {
order,
orderHash,
feeAmount: '100',
feeToken: CONTRACT_ADDRESSES.etherToken,
expiry: order.expiry,
takerSignature,
// trader: takerAddress,
// workflow: 'rfqm',
};
const response = {
proceedWithFill: false,
};
axiosMock
.onPost(`${makerUri}/rfqm/v2/sign`, JSON.parse(JSON.stringify(actualRequest)))
.replyOnce(HttpStatus.OK, response);
// When
const signature = await client.signV2Async(makerUri, 'dummy-integrator-id', request);
// Then
expect(signature).toBe(undefined);
});
});
});
});