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[] = [ { 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) => 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((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 { const metaTransactionPath = version === 'v1' ? META_TRANSACTION_V1_PATH : META_TRANSACTION_V2_PATH; const route = `${metaTransactionPath}/${endpoint}`; return await httpPostAsync({ app, route, body }); }