463 lines
19 KiB
TypeScript
463 lines
19 KiB
TypeScript
import { ErrorBody, GeneralErrorCodes, generalErrorCodeToReason, ValidationErrorCodes } from '@0x/api-utils';
|
|
import { expect } from 'chai';
|
|
import Web3ProviderEngine from 'web3-provider-engine';
|
|
import { BlockchainLifecycle } from 'dev-utils-deprecated';
|
|
import { BigNumber } from '@0x/utils';
|
|
import axios from 'axios';
|
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
|
import { Server } from 'http';
|
|
import * as HttpStatus from 'http-status-codes';
|
|
import * as _ from 'lodash';
|
|
import 'mocha';
|
|
|
|
import { getAppAsync } from '../src/app';
|
|
import { getDefaultAppDependenciesAsync } from '../src/runners/utils';
|
|
import { AppDependencies } from '../src/types';
|
|
import { LimitOrder } from '../src/asset-swapper';
|
|
import * as config from '../src/config';
|
|
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, NULL_ADDRESS, ONE_SECOND_MS, SRA_PATH } from '../src/constants';
|
|
import { OrderWatcherSignedOrderEntity } from '../src/entities';
|
|
import { SignedLimitOrder, SRAOrder } from '../src/types';
|
|
import { orderUtils } from '../src/utils/order_utils';
|
|
|
|
import {
|
|
CHAIN_ID,
|
|
ETHEREUM_RPC_URL,
|
|
getProvider,
|
|
MAX_MINT_AMOUNT,
|
|
WETH_TOKEN_ADDRESS,
|
|
ZRX_TOKEN_ADDRESS,
|
|
} from './constants';
|
|
import { setupDependenciesAsync, teardownDependenciesAsync } from './utils/deployment';
|
|
import { constructRoute, httpGetAsync, httpPostAsync } from './utils/http_utils';
|
|
import { getRandomSignedLimitOrderAsync } from './utils/orders';
|
|
import { Web3Wrapper } from '@0x/web3-wrapper';
|
|
|
|
// Force reload of the app avoid variables being polluted between test suites
|
|
delete require.cache[require.resolve('../src/app')];
|
|
delete require.cache[require.resolve('../src/runners/utils')];
|
|
|
|
const SUITE_NAME = 'Standard Relayer API (SRA) integration tests';
|
|
|
|
const EMPTY_PAGINATED_RESPONSE = {
|
|
perPage: DEFAULT_PER_PAGE,
|
|
page: DEFAULT_PAGE,
|
|
total: 0,
|
|
records: [],
|
|
};
|
|
|
|
const ONE_THOUSAND_IN_BASE = new BigNumber('1000000000000000000000');
|
|
|
|
const NOW = Math.floor(Date.now() / ONE_SECOND_MS);
|
|
const TOMORROW = new BigNumber(NOW + 24 * 3600);
|
|
|
|
describe(SUITE_NAME, () => {
|
|
let app: Express.Application;
|
|
let server: Server;
|
|
let dependencies: AppDependencies;
|
|
let makerAddress: string;
|
|
let otherAddress: string;
|
|
|
|
let blockchainLifecycle: BlockchainLifecycle;
|
|
let provider: Web3ProviderEngine;
|
|
|
|
async function addNewOrderAsync(
|
|
params: Partial<SignedLimitOrder> & { maker: string },
|
|
remainingFillableAmount?: BigNumber,
|
|
): Promise<SRAOrder> {
|
|
const limitOrder = await getRandomSignedLimitOrderAsync(provider, params);
|
|
const apiOrder: SRAOrder = {
|
|
order: limitOrder,
|
|
metaData: {
|
|
orderHash: new LimitOrder(limitOrder).getHash(),
|
|
remainingFillableTakerAmount: remainingFillableAmount || limitOrder.takerAmount,
|
|
},
|
|
};
|
|
const orderEntity = orderUtils.serializeOrder(apiOrder);
|
|
await dependencies.connection?.getRepository(OrderWatcherSignedOrderEntity).save(orderEntity);
|
|
return apiOrder;
|
|
}
|
|
|
|
before(async () => {
|
|
await setupDependenciesAsync(SUITE_NAME);
|
|
|
|
provider = getProvider();
|
|
// start the 0x-api app
|
|
dependencies = await getDefaultAppDependenciesAsync(provider, {
|
|
...config.defaultHttpServiceConfig,
|
|
ethereumRpcUrl: ETHEREUM_RPC_URL,
|
|
});
|
|
({ app, server } = await getAppAsync(
|
|
{ ...dependencies },
|
|
{ ...config.defaultHttpServiceConfig, ethereumRpcUrl: ETHEREUM_RPC_URL },
|
|
));
|
|
|
|
const web3Wrapper = new Web3Wrapper(provider);
|
|
blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
|
|
|
|
const accounts = await web3Wrapper.getAvailableAddressesAsync();
|
|
[makerAddress, otherAddress] = accounts;
|
|
});
|
|
after(async () => {
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((err?: Error) => {
|
|
if (err) {
|
|
reject(err);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
await teardownDependenciesAsync(SUITE_NAME);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await dependencies.connection?.runMigrations();
|
|
await blockchainLifecycle.startAsync();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await blockchainLifecycle.revertAsync();
|
|
await dependencies.connection
|
|
?.createQueryBuilder()
|
|
.delete()
|
|
.from(OrderWatcherSignedOrderEntity)
|
|
.where('true')
|
|
.execute();
|
|
});
|
|
|
|
describe('/fee_recipients', () => {
|
|
it('should return the list of fee recipients', async () => {
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/fee_recipients` });
|
|
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.type).to.eq('application/json');
|
|
expect(response.body).to.deep.eq({
|
|
...EMPTY_PAGINATED_RESPONSE,
|
|
total: 1,
|
|
records: [NULL_ADDRESS],
|
|
});
|
|
});
|
|
});
|
|
describe('GET /orders', () => {
|
|
it('should return empty response when no orders', async () => {
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/orders` });
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq(EMPTY_PAGINATED_RESPONSE);
|
|
});
|
|
it('should return orders in the local cache', async () => {
|
|
const apiOrder = await addNewOrderAsync({
|
|
maker: makerAddress,
|
|
});
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/orders` });
|
|
apiOrder.metaData.createdAt = response.body.records[0].metaData.createdAt; // createdAt is saved in the SignedOrders table directly
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq({
|
|
...EMPTY_PAGINATED_RESPONSE,
|
|
total: 1,
|
|
records: [JSON.parse(JSON.stringify(apiOrder))],
|
|
});
|
|
});
|
|
it('should return orders filtered by query params', async () => {
|
|
const apiOrder = await addNewOrderAsync({ maker: makerAddress });
|
|
const response = await httpGetAsync({
|
|
app,
|
|
route: `${SRA_PATH}/orders?maker=${apiOrder.order.maker}`,
|
|
});
|
|
apiOrder.metaData.createdAt = response.body.records[0].metaData.createdAt; // createdAt is saved in the SignedOrders table directly
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq({
|
|
...EMPTY_PAGINATED_RESPONSE,
|
|
total: 1,
|
|
records: [JSON.parse(JSON.stringify(apiOrder))],
|
|
});
|
|
});
|
|
it('should filter by order parameters AND trader', async () => {
|
|
const matchingOrders = await Promise.all([
|
|
addNewOrderAsync({
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
maker: makerAddress,
|
|
}),
|
|
addNewOrderAsync({
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
taker: makerAddress,
|
|
maker: otherAddress,
|
|
}),
|
|
]);
|
|
|
|
// Add anotther order that should not appear in the response.
|
|
await addNewOrderAsync({
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
maker: otherAddress,
|
|
});
|
|
const response = await httpGetAsync({
|
|
app,
|
|
route: `${SRA_PATH}/orders?makerToken=${ZRX_TOKEN_ADDRESS}&trader=${makerAddress}`,
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: fix me!
|
|
const sortByHash = (arr: any[]) => _.sortBy(arr, 'metaData.orderHash');
|
|
const { body } = response;
|
|
// Remove createdAt from response for easier comparison
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: fix me!
|
|
const cleanRecords = body.records.map((r: any) => _.omit(r, 'metaData.createdAt'));
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(body.total).to.eq(2);
|
|
expect(sortByHash(cleanRecords)).to.deep.eq(sortByHash(JSON.parse(JSON.stringify(matchingOrders))));
|
|
});
|
|
it('should return empty response when filtered by query params', async () => {
|
|
await addNewOrderAsync({ maker: makerAddress });
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/orders?maker=${NULL_ADDRESS}` });
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq(EMPTY_PAGINATED_RESPONSE);
|
|
});
|
|
it('should normalize addresses to lowercase', async () => {
|
|
const apiOrder = await addNewOrderAsync({ maker: makerAddress });
|
|
|
|
const makerUpperCase = `0x${apiOrder.order.maker.replace('0x', '').toUpperCase()}`;
|
|
const response = await httpGetAsync({
|
|
app,
|
|
route: `${SRA_PATH}/orders?maker=${makerUpperCase}`,
|
|
});
|
|
apiOrder.metaData.createdAt = response.body.records[0].metaData.createdAt; // createdAt is saved in the SignedOrders table directly
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq({
|
|
...EMPTY_PAGINATED_RESPONSE,
|
|
total: 1,
|
|
records: [JSON.parse(JSON.stringify(apiOrder))],
|
|
});
|
|
});
|
|
});
|
|
describe('GET /order', () => {
|
|
it('should return order by order hash', async () => {
|
|
const apiOrder = await addNewOrderAsync({ maker: makerAddress });
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/order/${apiOrder.metaData.orderHash}` });
|
|
apiOrder.metaData.createdAt = response.body.metaData.createdAt; // createdAt is saved in the SignedOrders table directly
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq(JSON.parse(JSON.stringify(apiOrder)));
|
|
});
|
|
it('should return 404 if order is not found', async () => {
|
|
const apiOrder = await addNewOrderAsync({ maker: makerAddress });
|
|
await dependencies.connection?.manager.delete(OrderWatcherSignedOrderEntity, apiOrder.metaData.orderHash);
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/order/${apiOrder.metaData.orderHash}` });
|
|
expect(response.status).to.deep.eq(HttpStatus.NOT_FOUND);
|
|
});
|
|
});
|
|
|
|
describe('GET /orderbook', () => {
|
|
it('should return orderbook for a given pair', async () => {
|
|
const apiOrder = await addNewOrderAsync({ maker: makerAddress });
|
|
const response = await httpGetAsync({
|
|
app,
|
|
route: constructRoute({
|
|
baseRoute: `${SRA_PATH}/orderbook`,
|
|
queryParams: {
|
|
baseToken: apiOrder.order.makerToken,
|
|
quoteToken: apiOrder.order.takerToken,
|
|
},
|
|
}),
|
|
});
|
|
apiOrder.metaData.createdAt = response.body.asks.records[0].metaData.createdAt; // createdAt is saved in the SignedOrders table directly
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
|
|
const expectedResponse = {
|
|
bids: EMPTY_PAGINATED_RESPONSE,
|
|
asks: {
|
|
...EMPTY_PAGINATED_RESPONSE,
|
|
total: 1,
|
|
records: [JSON.parse(JSON.stringify(apiOrder))],
|
|
},
|
|
};
|
|
expect(response.body).to.deep.eq(expectedResponse);
|
|
});
|
|
it('should return empty response if no matching orders', async () => {
|
|
const apiOrder = await addNewOrderAsync({ maker: makerAddress });
|
|
const response = await httpGetAsync({
|
|
app,
|
|
route: constructRoute({
|
|
baseRoute: `${SRA_PATH}/orderbook`,
|
|
queryParams: { baseToken: apiOrder.order.makerToken, quoteToken: NULL_ADDRESS },
|
|
}),
|
|
});
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq({
|
|
bids: EMPTY_PAGINATED_RESPONSE,
|
|
asks: EMPTY_PAGINATED_RESPONSE,
|
|
});
|
|
});
|
|
it('should return validation error if query params are missing', async () => {
|
|
const response = await httpGetAsync({ app, route: `${SRA_PATH}/orderbook?quoteToken=WETH` });
|
|
const validationErrors = {
|
|
code: 100,
|
|
reason: 'Validation Failed',
|
|
validationErrors: [
|
|
{
|
|
code: 1000,
|
|
field: 'baseToken',
|
|
reason: "should have required property 'baseToken'",
|
|
},
|
|
{
|
|
code: 1001,
|
|
field: 'quoteToken',
|
|
reason: 'should match pattern "^0x[0-9a-fA-F]{40}$"',
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.BAD_REQUEST);
|
|
expect(response.body).to.deep.eq(validationErrors);
|
|
});
|
|
});
|
|
describe('POST /order_config', () => {
|
|
it('should return 200 on success', async () => {
|
|
const order = await getRandomSignedLimitOrderAsync(provider, {
|
|
maker: makerAddress,
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
});
|
|
const expectedResponse = {
|
|
sender: NULL_ADDRESS,
|
|
feeRecipient: NULL_ADDRESS,
|
|
takerTokenFeeAmount: '0',
|
|
};
|
|
|
|
const response = await httpPostAsync({
|
|
app,
|
|
route: `${SRA_PATH}/order_config`,
|
|
body: {
|
|
...order,
|
|
expiry: TOMORROW,
|
|
},
|
|
});
|
|
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
expect(response.body).to.deep.eq(expectedResponse);
|
|
});
|
|
it('should return informative error when missing fields', async () => {
|
|
const order = await getRandomSignedLimitOrderAsync(provider, {
|
|
maker: makerAddress,
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
});
|
|
const validationError: ErrorBody = {
|
|
code: GeneralErrorCodes.ValidationError,
|
|
reason: generalErrorCodeToReason[GeneralErrorCodes.ValidationError],
|
|
validationErrors: [
|
|
{
|
|
field: 'taker',
|
|
code: ValidationErrorCodes.RequiredField,
|
|
reason: "should have required property 'taker'",
|
|
},
|
|
{
|
|
field: 'expiry',
|
|
code: ValidationErrorCodes.RequiredField,
|
|
reason: "should have required property 'expiry'",
|
|
},
|
|
],
|
|
};
|
|
const response = await httpPostAsync({
|
|
app,
|
|
route: `${SRA_PATH}/order_config`,
|
|
body: {
|
|
...order,
|
|
taker: undefined,
|
|
expiry: undefined,
|
|
},
|
|
});
|
|
expect(response.type).to.eq(`application/json`);
|
|
expect(response.status).to.eq(HttpStatus.BAD_REQUEST);
|
|
expect(response.body).to.deep.eq(validationError);
|
|
});
|
|
});
|
|
describe('POST /orders', () => {
|
|
it('should return HTTP OK on success', async () => {
|
|
const mockAxios = new AxiosMockAdapter(axios);
|
|
mockAxios.onPost(`${config.ORDER_WATCHER_URL}/orders`).reply(HttpStatus.OK);
|
|
|
|
const order = await getRandomSignedLimitOrderAsync(provider, {
|
|
maker: makerAddress,
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
makerAmount: MAX_MINT_AMOUNT,
|
|
takerAmount: ONE_THOUSAND_IN_BASE.multipliedBy(3),
|
|
chainId: CHAIN_ID,
|
|
expiry: TOMORROW,
|
|
});
|
|
|
|
const response = await httpPostAsync({
|
|
app,
|
|
route: `${SRA_PATH}/order`,
|
|
body: {
|
|
...order,
|
|
},
|
|
});
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
});
|
|
it('should respond before order watcher confirmation when ?skipConfirmation=true', async () => {
|
|
const mockAxios = new AxiosMockAdapter(axios);
|
|
mockAxios.onPost(`${config.ORDER_WATCHER_URL}/orders`).reply(HttpStatus.BAD_REQUEST);
|
|
|
|
const order = await getRandomSignedLimitOrderAsync(provider, {
|
|
maker: makerAddress,
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
makerAmount: MAX_MINT_AMOUNT,
|
|
takerAmount: ONE_THOUSAND_IN_BASE.multipliedBy(3),
|
|
chainId: CHAIN_ID,
|
|
expiry: TOMORROW,
|
|
});
|
|
const response = await httpPostAsync({
|
|
app,
|
|
route: `${SRA_PATH}/order?skipConfirmation=true`,
|
|
body: {
|
|
...order,
|
|
},
|
|
});
|
|
expect(response.status).to.eq(HttpStatus.OK);
|
|
});
|
|
it('should not skip confirmation normally', async () => {
|
|
const mockAxios = new AxiosMockAdapter(axios);
|
|
mockAxios.onPost().reply(HttpStatus.BAD_REQUEST);
|
|
|
|
const order = await getRandomSignedLimitOrderAsync(provider, {
|
|
maker: makerAddress,
|
|
makerToken: ZRX_TOKEN_ADDRESS,
|
|
takerToken: WETH_TOKEN_ADDRESS,
|
|
makerAmount: MAX_MINT_AMOUNT,
|
|
takerAmount: ONE_THOUSAND_IN_BASE.multipliedBy(3),
|
|
chainId: CHAIN_ID,
|
|
expiry: TOMORROW,
|
|
});
|
|
const response = await httpPostAsync({
|
|
app,
|
|
route: `${SRA_PATH}/order`,
|
|
body: {
|
|
...order,
|
|
},
|
|
});
|
|
expect(response.status).to.eq(HttpStatus.INTERNAL_SERVER_ERROR);
|
|
});
|
|
});
|
|
});
|