protocol/apps-node/api/test/meta_transaction_test.ts

1254 lines
59 KiB
TypeScript

import { WETH9Contract } from '@0x/contract-wrappers';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { BlockchainLifecycle } from 'dev-utils-deprecated';
import { isNativeSymbolOrAddress } from '@0x/token-metadata';
import { BigNumber } from '@0x/utils';
import Web3ProviderEngine from 'web3-provider-engine';
import { Server } from 'http';
import * as HttpStatus from 'http-status-codes';
import 'mocha';
import { expect } from 'chai';
import supertest from 'supertest';
import { getAppAsync } from '../src/app';
import { getDefaultAppDependenciesAsync } from '../src/runners/utils';
import { AppDependencies } from '../src/types';
import { LimitOrderFields } from '../src/asset-swapper';
import * as config from '../src/config';
import { META_TRANSACTION_V1_PATH, META_TRANSACTION_V2_PATH, ZERO } from '../src/constants';
import { getDBConnectionOrThrow } from '../src/db_connection';
import { ValidationErrorCodes, ValidationErrorReasons } from '../src/errors';
import { SignedLimitOrder } from '../src/types';
import { expectCorrectQuoteResponse, expectSwapError } from './test_utils';
import {
CHAIN_ID,
CONTRACT_ADDRESSES,
ETHEREUM_RPC_URL,
getProvider,
MAX_INT,
MAX_MINT_AMOUNT,
NULL_ADDRESS,
SYMBOL_TO_ADDRESS,
WETH_TOKEN_ADDRESS,
ZRX_TOKEN_ADDRESS,
} from './constants';
import { setupDependenciesAsync, teardownDependenciesAsync } from './utils/deployment';
import { httpPostAsync } from './utils/http_utils';
import { MockOrderWatcher } from './utils/mock_order_watcher';
import { getRandomSignedLimitOrderAsync } from './utils/orders';
import { decodeTransformERC20 } from './asset-swapper/test_utils/decoders';
import { decodeAffiliateFeeTransformerData } from '@0x/protocol-utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { randomAddress } from './utils/random';
// Force reload of the app avoid variables being polluted between test suites
// Warning: You probably don't want to move this
delete require.cache[require.resolve('../src/app')];
delete require.cache[require.resolve('../src/runners/utils')];
const SUITE_NAME = 'Meta-transaction API';
const MAKER_WETH_AMOUNT = new BigNumber('1000000000000000000');
const ONE_THOUSAND_IN_BASE = new BigNumber('1000000000000000000000');
const ZERO_EX_SOURCE = { name: '0x', proportion: new BigNumber('1') };
const INTEGRATOR_ID = 'integrator';
const TAKER_ADDRESS = '0x70a9f34f9b34c64957b9c401a97bfed35b95049e';
const onChainBilling = 'on-chain';
const offChainBilling = 'off-chain';
describe(SUITE_NAME, () => {
let app: Express.Application;
let server: Server;
let dependencies: AppDependencies;
let accounts: string[];
let takerAddress: string;
let makerAddress: string;
const invalidTakerAddress = '0x0000000000000000000000000000000000000001';
let blockchainLifecycle: BlockchainLifecycle;
let provider: Web3ProviderEngine;
before(async () => {
await setupDependenciesAsync(SUITE_NAME);
const connection = await getDBConnectionOrThrow();
await connection.runMigrations();
provider = getProvider();
const web3Wrapper = new Web3Wrapper(provider);
blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const mockOrderWatcher = new MockOrderWatcher(connection);
accounts = await web3Wrapper.getAvailableAddressesAsync();
[, makerAddress, takerAddress] = accounts;
// Set up liquidity.
await blockchainLifecycle.startAsync();
const wethToken = new WETH9Contract(CONTRACT_ADDRESSES.etherToken, provider);
const zrxToken = new DummyERC20TokenContract(CONTRACT_ADDRESSES.zrxToken, provider);
// EP setup so maker address can take
await zrxToken.mint(MAX_MINT_AMOUNT).awaitTransactionSuccessAsync({ from: takerAddress });
await zrxToken.mint(MAX_MINT_AMOUNT).awaitTransactionSuccessAsync({ from: makerAddress });
await wethToken.deposit().awaitTransactionSuccessAsync({ from: takerAddress, value: MAKER_WETH_AMOUNT });
await wethToken.deposit().awaitTransactionSuccessAsync({ from: makerAddress, value: MAKER_WETH_AMOUNT });
await wethToken
.approve(CONTRACT_ADDRESSES.exchangeProxy, MAX_INT)
.awaitTransactionSuccessAsync({ from: takerAddress });
await wethToken
.approve(CONTRACT_ADDRESSES.exchangeProxy, MAX_INT)
.awaitTransactionSuccessAsync({ from: makerAddress });
await zrxToken
.approve(CONTRACT_ADDRESSES.exchangeProxy, MAX_INT)
.awaitTransactionSuccessAsync({ from: takerAddress });
await zrxToken
.approve(CONTRACT_ADDRESSES.exchangeProxy, MAX_INT)
.awaitTransactionSuccessAsync({ from: makerAddress });
const limitOrders: Partial<LimitOrderFields>[] = [
{
makerToken: ZRX_TOKEN_ADDRESS,
takerToken: WETH_TOKEN_ADDRESS,
makerAmount: ONE_THOUSAND_IN_BASE,
takerAmount: ONE_THOUSAND_IN_BASE,
maker: makerAddress,
},
{
makerToken: ZRX_TOKEN_ADDRESS,
takerToken: WETH_TOKEN_ADDRESS,
makerAmount: ONE_THOUSAND_IN_BASE,
takerAmount: ONE_THOUSAND_IN_BASE.multipliedBy(2),
maker: makerAddress,
},
{
makerToken: ZRX_TOKEN_ADDRESS,
takerToken: WETH_TOKEN_ADDRESS,
makerAmount: MAX_MINT_AMOUNT,
takerAmount: ONE_THOUSAND_IN_BASE.multipliedBy(3),
maker: makerAddress,
},
{
makerToken: WETH_TOKEN_ADDRESS,
takerToken: ZRX_TOKEN_ADDRESS,
makerAmount: MAKER_WETH_AMOUNT,
takerAmount: ONE_THOUSAND_IN_BASE,
maker: makerAddress,
},
];
const signPartialOrder = (order: Partial<LimitOrderFields>) => getRandomSignedLimitOrderAsync(provider, order);
const signedOrders: SignedLimitOrder[] = await Promise.all(limitOrders.map(signPartialOrder));
await mockOrderWatcher.postOrdersAsync(signedOrders);
// 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 },
));
});
after(async () => {
await blockchainLifecycle.revertAsync();
await new Promise<void>((resolve, reject) => {
server.close((err?: Error) => {
if (err) {
reject(err);
}
resolve();
});
});
await teardownDependenciesAsync(SUITE_NAME);
});
describe('v2 /price', async () => {
describe('metaTransaction v1 requested', async () => {
it('should respond with 200 OK even if the the takerAddress cannot complete a trade', async () => {
// The taker does not have an allowance
const swapResponse = await requestSwap(app, 'price', 'v2', {
takerAddress: invalidTakerAddress,
sellToken: 'WETH',
buyToken: 'ZRX',
sellAmount: '10000',
integratorId: 'integrator',
metaTransactionVersion: 'v1',
});
expect(swapResponse.statusCode).eq(HttpStatus.StatusCodes.OK);
});
});
// describe('metaTransaction v2 requested', async () => {
// it('should respond with 200 OK even if the the takerAddress cannot complete a trade', async () => {
// // The taker does not have an allowance
// const swapResponse = await requestSwap(app, 'price', 'v2', {
// takerAddress: invalidTakerAddress,
// sellToken: 'WETH',
// buyToken: 'ZRX',
// sellAmount: '10000',
// integratorId: 'integrator',
// metaTransactionVersion: 'v2',
// });
// expect(swapResponse.statusCode).eq(HttpStatus.StatusCodes.OK);
// });
// });
});
describe('v2 /quote', async () => {
it('should handle valid request body permutations', async () => {
const WETH_BUY_AMOUNT = MAKER_WETH_AMOUNT.div(10).toString();
const ZRX_BUY_AMOUNT = ONE_THOUSAND_IN_BASE.div(10).toString();
const bodyPermutations = [
{
buyToken: 'ZRX',
sellToken: 'WETH',
buyAmount: ZRX_BUY_AMOUNT,
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
},
{
buyToken: 'ZRX',
sellToken: 'WETH',
buyAmount: ZRX_BUY_AMOUNT,
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '0.2',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
gasFee: {
type: 'gas',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
},
},
{
buyToken: 'WETH',
sellToken: 'ZRX',
buyAmount: WETH_BUY_AMOUNT,
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '0.2',
billingType: offChainBilling as 'off-chain',
feeRecipient: null,
},
},
},
{
buyToken: ZRX_TOKEN_ADDRESS,
sellToken: 'WETH',
buyAmount: ZRX_BUY_AMOUNT,
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
gasFee: {
type: 'gas',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
},
},
{
buyToken: ZRX_TOKEN_ADDRESS,
sellToken: WETH_TOKEN_ADDRESS,
buyAmount: ZRX_BUY_AMOUNT,
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling as 'on-chain',
feeRecipient: randomAddress(),
},
},
},
{
buyToken: ZRX_TOKEN_ADDRESS,
sellToken: WETH_TOKEN_ADDRESS,
buyAmount: ZRX_BUY_AMOUNT,
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
feeConfigs: {
gasFee: { type: 'gas', billingType: offChainBilling as 'off-chain', feeRecipient: null },
},
},
];
for (const body of bodyPermutations) {
// metaTransaction v1 requested
const [response1 /* response2 */] = await Promise.all([
requestSwap(app, 'quote', 'v2', {
...body,
metaTransactionVersion: 'v1',
}),
// await requestSwap(app, 'quote', 'v2', {
// ...body,
// metaTransactionVersion: 'v2',
// }),
]);
expectCorrectQuoteResponse(response1, {
buyAmount: new BigNumber(body.buyAmount),
sellTokenAddress: body.sellToken.startsWith('0x')
? body.sellToken
: SYMBOL_TO_ADDRESS[body.sellToken],
buyTokenAddress: body.buyToken.startsWith('0x') ? body.buyToken : SYMBOL_TO_ADDRESS[body.buyToken],
allowanceTarget: isNativeSymbolOrAddress(body.sellToken, CHAIN_ID)
? NULL_ADDRESS
: CONTRACT_ADDRESSES.exchangeProxy,
sources: [ZERO_EX_SOURCE],
});
// expectCorrectQuoteResponse(response2, {
// buyAmount: new BigNumber(body.buyAmount),
// sellTokenAddress: body.sellToken.startsWith('0x')
// ? body.sellToken
// : SYMBOL_TO_ADDRESS[body.sellToken],
// buyTokenAddress: body.buyToken.startsWith('0x') ? body.buyToken : SYMBOL_TO_ADDRESS[body.buyToken],
// allowanceTarget: isNativeSymbolOrAddress(body.sellToken, CHAIN_ID)
// ? NULL_ADDRESS
// : CONTRACT_ADDRESSES.exchangeProxy,
// sources: [ZERO_EX_SOURCE],
// });
}
});
describe('metaTransactionVersion param is v1', async () => {
it("should respond with INSUFFICIENT_ASSET_LIQUIDITY when there's no liquidity", async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: ZRX_TOKEN_ADDRESS,
sellToken: WETH_TOKEN_ADDRESS,
buyAmount: '10000000000000000000000000000000',
integratorId: 'integrator',
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
code: ValidationErrorCodes.ValueOutOfRange,
field: 'buyAmount',
reason: 'INSUFFICIENT_ASSET_LIQUIDITY',
},
],
});
});
it('should respect buyAmount', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
buyAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '0.2',
billingType: offChainBilling,
feeRecipient: null,
},
gasFee: { type: 'gas', billingType: offChainBilling, feeRecipient: null },
},
});
expectCorrectQuoteResponse(response, { buyAmount: new BigNumber(1234) });
});
it('should respect sellAmount', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '0.2',
billingType: offChainBilling,
feeRecipient: null,
},
},
});
expectCorrectQuoteResponse(response, { sellAmount: new BigNumber(1234) });
});
it('should return the correct trade kind', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '0.2',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expect(response.body.trade.kind).to.eql('metatransaction');
});
it('should return the correct non fee-related meta-transaction fields', async () => {
const feeRecipient = randomAddress();
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
zeroExFee: {
type: 'volume',
volumePercentage: '0.2',
billingType: onChainBilling,
feeRecipient,
},
},
});
const { trade } = response.body;
expect(trade.kind).to.eql('metatransaction');
expect(trade.metaTransaction.signer).to.eql(TAKER_ADDRESS);
expect(trade.metaTransaction.verifyingContract).to.eql(CONTRACT_ADDRESSES.exchangeProxy);
});
describe('fee', async () => {
describe('integrator', async () => {
it('should throw error if integrator fee type is invalid', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'random',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.IncorrectFormat,
reason: ValidationErrorReasons.InvalidGaslessFeeType,
},
],
});
});
it('should throw error if volumePercentage is out of range', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '1000',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.ValueOutOfRange,
reason: ValidationErrorReasons.PercentageOutOfRange,
},
],
});
});
it('should returns correct integrator fee', async () => {
const feeRecipient = randomAddress();
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient,
},
},
});
const { sellAmount, trade, fees } = response.body;
const metaTransaction = trade.metaTransaction;
const callArgs = decodeTransformERC20(metaTransaction.callData);
expect(sellAmount).to.eql('1234');
expect(fees.integratorFee).to.eql({
type: 'volume',
feeToken: WETH_TOKEN_ADDRESS,
billingType: onChainBilling,
feeRecipient,
feeAmount: '123',
volumePercentage: '0.1',
});
expect(trade.kind).to.eql('metatransaction');
expect(metaTransaction.signer).to.eql(TAKER_ADDRESS);
expect(metaTransaction.feeToken).to.eql(NULL_ADDRESS);
expect(metaTransaction.feeAmount).to.eql(ZERO.toString());
expect(metaTransaction.verifyingContract).to.eql(CONTRACT_ADDRESSES.exchangeProxy);
expect(decodeAffiliateFeeTransformerData(callArgs.transformations[0].data).fees).to.eql([
{ token: WETH_TOKEN_ADDRESS, amount: new BigNumber(123), recipient: feeRecipient },
]);
});
});
describe('0x', async () => {
it('should throw error if 0x fee type is invalid', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
zeroExFee: {
type: 'random',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.IncorrectFormat,
reason: ValidationErrorReasons.InvalidGaslessFeeType,
},
],
});
});
it('should throw error if volumePercentage is out of range', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
zeroExFee: {
type: 'volume',
volumePercentage: '1000',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.ValueOutOfRange,
reason: ValidationErrorReasons.PercentageOutOfRange,
},
],
});
});
it('should throw error if integrator fee config is empty and 0x fee kind is integrator_share', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '1000',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.IncorrectFormat,
reason: ValidationErrorReasons.InvalidGaslessFeeType,
},
],
});
it('should throw error if integratorSharePercentage is out of range', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
integratorFee: {
type: 'volume',
volumePercentage: '0.1',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
zeroExFee: {
type: 'integrator_share',
integratorSharePercentage: '1000',
billingType: onChainBilling,
feeRecipient: randomAddress(),
},
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.ValueOutOfRange,
reason: ValidationErrorReasons.PercentageOutOfRange,
},
],
});
});
});
it('should returns correct 0x fee', async () => {
const feeRecipient = randomAddress();
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
zeroExFee: {
type: 'volume',
volumePercentage: '0.2',
billingType: onChainBilling,
feeRecipient,
},
},
});
const { sellAmount, trade, fees } = response.body;
const metaTransaction = trade.metaTransaction;
const callArgs = decodeTransformERC20(metaTransaction.callData);
expect(sellAmount).to.eql('1234');
expect(fees.zeroExFee).to.eql({
type: 'volume',
feeToken: WETH_TOKEN_ADDRESS,
billingType: onChainBilling,
feeRecipient,
feeAmount: '246',
volumePercentage: '0.2',
});
expect(trade.kind).to.eql('metatransaction');
expect(metaTransaction.signer).to.eql(TAKER_ADDRESS);
expect(metaTransaction.feeToken).to.eql(NULL_ADDRESS);
expect(metaTransaction.feeAmount).to.eql(ZERO.toString());
expect(metaTransaction.verifyingContract).to.eql(CONTRACT_ADDRESSES.exchangeProxy);
expect(decodeAffiliateFeeTransformerData(callArgs.transformations[0].data).fees).to.eql([
{ token: WETH_TOKEN_ADDRESS, amount: new BigNumber(246), recipient: feeRecipient },
]);
});
});
describe('gas', async () => {
it('should throw error if gas fee type is invalid', async () => {
const response = await requestSwap(app, 'quote', 'v2', {
buyToken: 'ZRX',
sellToken: 'WETH',
sellAmount: '1234',
integratorId: INTEGRATOR_ID,
takerAddress: TAKER_ADDRESS,
metaTransactionVersion: 'v1',
feeConfigs: {
gasFee: { type: 'random', billingType: onChainBilling, feeRecipient: randomAddress() },
},
});
expectSwapError(response, {
validationErrors: [
{
field: 'feeConfigs',
code: ValidationErrorCodes.IncorrectFormat,
reason: ValidationErrorReasons.InvalidGaslessFeeType,
},
],
});
});
});
});
});
// describe('metaTransactionVersion param is v2', async () => {
// it("should respond with INSUFFICIENT_ASSET_LIQUIDITY when there's no liquidity", async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: ZRX_TOKEN_ADDRESS,
// sellToken: WETH_TOKEN_ADDRESS,
// buyAmount: '10000000000000000000000000000000',
// integratorId: 'integrator',
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// code: ValidationErrorCodes.ValueOutOfRange,
// field: 'buyAmount',
// reason: 'INSUFFICIENT_ASSET_LIQUIDITY',
// },
// ],
// });
// });
// it('should respect buyAmount', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// buyAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// zeroExFee: {
// type: 'integrator_share',
// integratorSharePercentage: '0.2',
// billingType: offChainBilling,
// feeRecipient: null,
// },
// gasFee: { type: 'gas', billingType: offChainBilling, feeRecipient: null },
// },
// });
// expectCorrectQuoteResponse(response, { buyAmount: new BigNumber(1234) });
// });
// it('should respect sellAmount', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// zeroExFee: {
// type: 'integrator_share',
// integratorSharePercentage: '0.2',
// billingType: offChainBilling,
// feeRecipient: null,
// },
// },
// });
// expectCorrectQuoteResponse(response, { sellAmount: new BigNumber(1234) });
// });
// it('should returns the correct trade kind', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// zeroExFee: {
// type: 'integrator_share',
// integratorSharePercentage: '0.2',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expect(response.body.trade.kind).to.eql('metatransaction_v2');
// });
// it('should return the correct non fee-related meta-transaction fields', async () => {
// const feeRecipient = randomAddress();
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient,
// },
// },
// });
// const { trade } = response.body;
// expect(trade.kind).to.eql('metatransaction_v2');
// expect(trade.metaTransaction.signer).to.eql(TAKER_ADDRESS);
// expect(trade.metaTransaction.verifyingContract).to.eql(CONTRACT_ADDRESSES.exchangeProxy);
// });
// describe('fee', async () => {
// describe('integrator', async () => {
// it('should throw error if integrator fee type is invalid', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'random',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.IncorrectFormat,
// reason: ValidationErrorReasons.InvalidGaslessFeeType,
// },
// ],
// });
// });
// it('should throw error if volumePercentage is out of range', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '1000',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.ValueOutOfRange,
// reason: ValidationErrorReasons.PercentageOutOfRange,
// },
// ],
// });
// });
// it('should returns correct integrator fee', async () => {
// const feeRecipient = randomAddress();
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient,
// },
// },
// });
// const { sellAmount, trade, fees } = response.body;
// const metaTransaction = trade.metaTransaction;
// const callArgs = decodeTransformERC20(metaTransaction.callData);
// expect(sellAmount).to.eql('1234');
// expect(fees.integratorFee).to.eql({
// type: 'volume',
// feeToken: WETH_TOKEN_ADDRESS,
// billingType: onChainBilling,
// feeRecipient,
// feeAmount: '123',
// volumePercentage: '0.1',
// });
// expect(trade.kind).to.eql('metatransaction_v2');
// expect(metaTransaction.signer).to.eql(TAKER_ADDRESS);
// expect(metaTransaction.verifyingContract).to.eql(CONTRACT_ADDRESSES.exchangeProxy);
// expect(metaTransaction.feeToken).to.eql(WETH_TOKEN_ADDRESS);
// expect(metaTransaction.fees).to.eql([{ amount: '123', recipient: feeRecipient }]);
// // Make sure the first transformer in this case is fill quote transformer
// // since if `metaTransactionVersion` is v2, sell token fees are not transferred by affiliate fee transformer
// // but in `executeMetaTransactionV2` instead
// expect(decodeFillQuoteTransformerData(callArgs.transformations[0].data).side).to.eql(
// FillQuoteTransformerSide.Sell,
// );
// });
// });
// describe('0x', async () => {
// it('should throw error if 0x fee type is invalid', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// zeroExFee: {
// type: 'random',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.IncorrectFormat,
// reason: ValidationErrorReasons.InvalidGaslessFeeType,
// },
// ],
// });
// });
// it('should throw error if volumePercentage is out of range', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// zeroExFee: {
// type: 'volume',
// volumePercentage: '1000',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.ValueOutOfRange,
// reason: ValidationErrorReasons.PercentageOutOfRange,
// },
// ],
// });
// });
// it('should throw error if integrator fee config is empty and 0x fee kind is integrator_share', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// zeroExFee: {
// type: 'integrator_share',
// integratorSharePercentage: '1000',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.IncorrectFormat,
// reason: ValidationErrorReasons.InvalidGaslessFeeType,
// },
// ],
// });
// it('should throw error if integratorSharePercentage is out of range', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// integratorFee: {
// type: 'volume',
// volumePercentage: '0.1',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// zeroExFee: {
// type: 'integrator_share',
// integratorSharePercentage: '1000',
// billingType: onChainBilling,
// feeRecipient: randomAddress(),
// },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.ValueOutOfRange,
// reason: ValidationErrorReasons.PercentageOutOfRange,
// },
// ],
// });
// });
// });
// it('should returns correct 0x fee', async () => {
// const feeRecipient = randomAddress();
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// zeroExFee: {
// type: 'volume',
// volumePercentage: '0.2',
// billingType: onChainBilling,
// feeRecipient,
// },
// },
// });
// const { sellAmount, trade, fees } = response.body;
// const metaTransaction = trade.metaTransaction;
// const callArgs = decodeTransformERC20(metaTransaction.callData);
// expect(sellAmount).to.eql('1234');
// expect(fees.zeroExFee).to.eql({
// type: 'volume',
// feeToken: WETH_TOKEN_ADDRESS,
// billingType: onChainBilling,
// feeRecipient,
// feeAmount: '246',
// volumePercentage: '0.2',
// });
// expect(trade.kind).to.eql('metatransaction_v2');
// expect(metaTransaction.signer).to.eql(TAKER_ADDRESS);
// expect(metaTransaction.verifyingContract).to.eql(CONTRACT_ADDRESSES.exchangeProxy);
// expect(metaTransaction.feeToken).to.eql(WETH_TOKEN_ADDRESS);
// expect(metaTransaction.fees).to.eql([{ amount: '246', recipient: feeRecipient }]);
// // Make sure the first transformer in this case is fill quote transformer
// // since if `metaTransactionVersion` is v2, sell token fees are not transferred by affiliate fee transformer
// // but in `executeMetaTransactionV2` instead
// expect(decodeFillQuoteTransformerData(callArgs.transformations[0].data).side).to.eql(
// FillQuoteTransformerSide.Sell,
// );
// });
// });
// describe('gas', async () => {
// it('should throw error if gas fee type is invalid', async () => {
// const response = await requestSwap(app, 'quote', 'v2', {
// buyToken: 'ZRX',
// sellToken: 'WETH',
// sellAmount: '1234',
// integratorId: INTEGRATOR_ID,
// takerAddress: TAKER_ADDRESS,
// metaTransactionVersion: 'v2',
// feeConfigs: {
// gasFee: { type: 'random', billingType: onChainBilling, feeRecipient: randomAddress() },
// },
// });
// expectSwapError(response, {
// validationErrors: [
// {
// field: 'feeConfigs',
// code: ValidationErrorCodes.IncorrectFormat,
// reason: ValidationErrorReasons.InvalidGaslessFeeType,
// },
// ],
// });
// });
// });
// });
// });
});
});
async function requestSwap(
app: Express.Application,
endpoint: 'price' | 'quote',
version: 'v1' | 'v2',
body: {
buyToken: string;
buyAmount?: string;
sellToken: string;
sellAmount?: string;
takerAddress: string;
slippagePercentage?: string;
integratorId: string;
quoteUniqueId?: string;
metaTransactionVersion?: 'v1' | 'v2';
feeConfigs?: {
integratorFee?: {
type: string;
volumePercentage: string;
billingType: 'on-chain' | 'off-chain';
feeRecipient: string;
};
zeroExFee?:
| {
type: string;
volumePercentage: string;
billingType: 'on-chain' | 'off-chain';
feeRecipient: string | null;
}
| {
type: string;
integratorSharePercentage: string;
billingType: 'on-chain' | 'off-chain';
feeRecipient: string | null;
};
gasFee?: {
type: string;
feeRecipient: string | null;
billingType: 'on-chain' | 'off-chain';
};
};
},
): Promise<supertest.Response> {
const metaTransactionPath = version === 'v1' ? META_TRANSACTION_V1_PATH : META_TRANSACTION_V2_PATH;
const route = `${metaTransactionPath}/${endpoint}`;
return await httpPostAsync({ app, route, body });
}