diff --git a/contracts/integrations/test/aggregation/fill_test.ts b/contracts/integrations/test/aggregation/fill_test.ts deleted file mode 100644 index 14c1eef3bc..0000000000 --- a/contracts/integrations/test/aggregation/fill_test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { MarketBuySwapQuote, MarketSellSwapQuote, Orderbook, SwapQuoter } from '@0x/asset-swapper'; -import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; -import { assetDataUtils } from '@0x/order-utils'; -import { FillResults, SignedOrder } from '@0x/types'; -import { BigNumber, logUtils } from '@0x/utils'; -import * as _ from 'lodash'; - -import { TestMainnetAggregatorFillsContract } from '../wrappers'; - -import { tokens } from './tokens'; - -blockchainTests.live('Aggregator Mainnet Tests', env => { - // Mainnet address of the `TestMainnetAggregatorFills` contract. - const TEST_CONTRACT_ADDRESS = '0x37Ca306F42748b7fe105F89FCBb2CD03D27c8146'; - const TAKER_ADDRESS = '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'; // Vitalik - const ORDERBOOK_POLLING_MS = 1000; - const GAS_PRICE = new BigNumber(1); - const TAKER_ASSET_ETH_VALUE = 500e18; - const MIN_BALANCE = 500.1e18; - const SYMBOLS = ['ETH', 'DAI', 'USDC', 'FOAM']; - const TEST_PAIRS = _.flatten(SYMBOLS.map(m => SYMBOLS.filter(t => t !== m).map(t => [m, t]))); - const FILL_VALUES = [1, 10, 1e2, 1e3, 1e4, 2.5e4, 5e4]; - - let testContract: TestMainnetAggregatorFillsContract; - let swapQuoter: SwapQuoter; - let takerEthBalance: BigNumber; - const orderbooks: { [name: string]: Orderbook } = {}; - - async function getTakerOrdersAsync(takerAssetSymbol: string): Promise { - if (takerAssetSymbol === 'ETH') { - return []; - } - return getOrdersAsync(takerAssetSymbol, 'ETH'); - } - - // Fetches ETH -> taker asset orders for the forwarder contract. - async function getOrdersAsync(makerAssetSymbol: string, takerAssetSymbol: string): Promise { - const takerTokenAddress = tokens[takerAssetSymbol].address; - const makerTokenAddress = tokens[makerAssetSymbol].address; - const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); - const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); - const orders = _.flatten( - await Promise.all( - Object.keys(orderbooks).map(async name => - getOrdersFromOrderBookAsync(name, makerAssetData, takerAssetData), - ), - ), - ); - const uniqueOrders: SignedOrder[] = []; - for (const order of orders) { - if (!order.makerFee.eq(0) || !order.takerFee.eq(0)) { - continue; - } - if (uniqueOrders.findIndex(o => isSameOrder(order, o)) === -1) { - uniqueOrders.push(order); - } - } - return uniqueOrders; - } - - async function getOrdersFromOrderBookAsync( - name: string, - makerAssetData: string, - takerAssetData: string, - ): Promise { - try { - return (await orderbooks[name].getOrdersAsync(makerAssetData, takerAssetData)).map(r => r.order); - } catch (err) { - logUtils.warn(`Failed to retrieve orders from orderbook "${name}".`); - } - return []; - } - - function isSameOrder(a: SignedOrder, b: SignedOrder): boolean { - for (const [k, v] of Object.entries(a)) { - if (k in (b as any)) { - if (BigNumber.isBigNumber(v) && !v.eq((b as any)[k])) { - return false; - } - if (v !== (b as any)[k]) { - return false; - } - } - } - return true; - } - - function toTokenUnits(symbol: string, weis: Numberish): BigNumber { - return new BigNumber(weis).div(new BigNumber(10).pow(tokens[symbol].decimals)); - } - - function fromTokenUnits(symbol: string, units: Numberish): BigNumber { - return new BigNumber(units) - .times(new BigNumber(10).pow(tokens[symbol].decimals)) - .integerValue(BigNumber.ROUND_DOWN); - } - - interface MarketOperationResult { - makerAssetBalanceBefore: BigNumber; - takerAssetBalanceBefore: BigNumber; - makerAssetBalanceAfter: BigNumber; - takerAssetBalanceAfter: BigNumber; - fillResults: FillResults; - } - - // Liquidity is low right now so it's possible we didn't have - // enough taker assets to cover the orders, so occasionally we'll get incomplete - // fills. This function will catch those cases. - // TODO(dorothy-zbornak): Remove this special case when liquidity is up. - function checkHadEnoughTakerAsset( - quote: MarketBuySwapQuote | MarketSellSwapQuote, - result: MarketOperationResult, - ): boolean { - if (result.takerAssetBalanceBefore.gte(quote.worstCaseQuoteInfo.takerAssetAmount)) { - return true; - } - const takerAssetPct = result.takerAssetBalanceBefore - .div(quote.worstCaseQuoteInfo.takerAssetAmount) - .times(100) - .toNumber() - .toFixed(1); - logUtils.warn(`Could not acquire enough taker asset to complete the fill: ${takerAssetPct}%`); - expect(result.fillResults.makerAssetFilledAmount).to.bignumber.lt(quote.worstCaseQuoteInfo.makerAssetAmount); - return false; - } - - before(async () => { - testContract = new TestMainnetAggregatorFillsContract(TEST_CONTRACT_ADDRESS, env.provider, { - ...env.txDefaults, - gasPrice: GAS_PRICE, - gas: 10e6, - }); - swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl(env.provider, 'https://api.0x.org/sra'); - // Pool orderbooks because we're desperate for liquidity. - orderbooks.swapQuoter = swapQuoter.orderbook; - orderbooks.bamboo = Orderbook.getOrderbookForPollingProvider({ - httpEndpoint: 'https://sra.bamboorelay.com/0x/v3', - pollingIntervalMs: ORDERBOOK_POLLING_MS, - }); - // TODO(dorothy-zbornak): Uncomment when radar's SRA is up. - // orderbooks.radar = Orderbook.getOrderbookForPollingProvider({ - // httpEndpoint: 'https://api-v3.radarrelay.com/v3', - // pollingIntervalMs: ORDERBOOK_POLLING_MS, - // }); - takerEthBalance = await env.web3Wrapper.getBalanceInWeiAsync(TAKER_ADDRESS); - }); - - it('taker has minimum ETH', async () => { - expect(takerEthBalance).to.bignumber.gte(MIN_BALANCE); - }); - - describe('market sells', () => { - for (const [makerSymbol, takerSymbol] of TEST_PAIRS) { - for (const fillValue of FILL_VALUES) { - const fillAmount = fromTokenUnits(takerSymbol, new BigNumber(fillValue).div(tokens[takerSymbol].price)); - it(`sell ${toTokenUnits(takerSymbol, fillAmount)} ${takerSymbol} for ${makerSymbol}`, async () => { - const [quote, takerOrders] = await Promise.all([ - swapQuoter.getMarketSellSwapQuoteAsync( - tokens[makerSymbol].address, - tokens[takerSymbol].address, - fillAmount, - { gasPrice: GAS_PRICE }, - ), - getTakerOrdersAsync(takerSymbol), - ]); - // Buy taker assets from `takerOrders` and and perform a - // market sell on the bridge orders. - const fill = await testContract - .marketSell( - tokens[makerSymbol].address, - tokens[takerSymbol].address, - quote.orders, - takerOrders, - quote.orders.map(o => o.signature), - takerOrders.map(o => o.signature), - quote.takerAssetFillAmount, - ) - .callAsync({ - value: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount.plus(TAKER_ASSET_ETH_VALUE), - from: TAKER_ADDRESS, - gasPrice: quote.gasPrice, - }); - if (checkHadEnoughTakerAsset(quote, fill)) { - expect(fill.fillResults.makerAssetFilledAmount, 'makerAssetFilledAmount').to.bignumber.gte( - quote.worstCaseQuoteInfo.makerAssetAmount, - ); - expect(fill.fillResults.takerAssetFilledAmount, 'takerAssetFilledAmount').to.bignumber.lte( - quote.takerAssetFillAmount, - ); - } - }); - } - } - }); - - describe('market buys', () => { - for (const [makerSymbol, takerSymbol] of TEST_PAIRS) { - for (const fillValue of FILL_VALUES) { - const fillAmount = fromTokenUnits(makerSymbol, new BigNumber(fillValue).div(tokens[makerSymbol].price)); - it(`buy ${toTokenUnits(makerSymbol, fillAmount)} ${makerSymbol} with ${takerSymbol}`, async () => { - const [quote, takerOrders] = await Promise.all([ - swapQuoter.getMarketBuySwapQuoteAsync( - tokens[makerSymbol].address, - tokens[takerSymbol].address, - fillAmount, - { gasPrice: GAS_PRICE }, - ), - getTakerOrdersAsync(takerSymbol), - ]); - // Buy taker assets from `takerOrders` and and perform a - // market buy on the bridge orders. - const fill = await testContract - .marketBuy( - tokens[makerSymbol].address, - tokens[takerSymbol].address, - quote.orders, - takerOrders, - quote.orders.map(o => o.signature), - takerOrders.map(o => o.signature), - quote.makerAssetFillAmount, - ) - .callAsync({ - value: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount.plus(TAKER_ASSET_ETH_VALUE), - from: TAKER_ADDRESS, - gasPrice: quote.gasPrice, - }); - if (checkHadEnoughTakerAsset(quote, fill)) { - expect(fill.fillResults.takerAssetFilledAmount, 'takerAssetFilledAmount').to.bignumber.lte( - quote.worstCaseQuoteInfo.takerAssetAmount, - ); - expect(fill.fillResults.makerAssetFilledAmount, 'makerAssetFilledAmount').to.bignumber.gte( - quote.makerAssetFillAmount, - ); - } - }); - } - } - }); -}); diff --git a/contracts/integrations/test/aggregation/tokens.ts b/contracts/integrations/test/aggregation/tokens.ts deleted file mode 100644 index 5fc3f29e2a..0000000000 --- a/contracts/integrations/test/aggregation/tokens.ts +++ /dev/null @@ -1,77 +0,0 @@ -export const tokens: { [symbol: string]: { address: string; decimals: number; price: number } } = { - ETH: { - address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - decimals: 18, - price: 133, - }, - SAI: { - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - decimals: 18, - price: 1, - }, - DAI: { - address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', - decimals: 18, - price: 1, - }, - USDC: { - address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - decimals: 6, - price: 1, - }, - WBTC: { - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - decimals: 8, - price: 6900, - }, - MKR: { - address: '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', - decimals: 18, - price: 454, - }, - BAT: { - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - decimals: 18, - price: 0.17, - }, - OMG: { - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - decimals: 18, - price: 0.65, - }, - ZRX: { - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - decimals: 18, - price: 0.19, - }, - ZIL: { - address: '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27', - decimals: 12, - price: 0.004, - }, - FOAM: { - address: '0x4946Fcea7C692606e8908002e55A582af44AC121', - decimals: 18, - price: 0.004, - }, - USDT: { - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - decimals: 6, - price: 0.019, - }, - REP: { - address: '0x1985365e9f78359a9B6AD760e32412f4a445E862', - decimals: 18, - price: 8.9, - }, - MANA: { - address: '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942', - decimals: 18, - price: 0.025, - }, - LINK: { - address: '0x514910771AF9Ca656af840dff83E8264EcF986CA', - decimals: 18, - price: 1.8, - }, -}; diff --git a/contracts/integrations/test/exchange-proxy/mtx_test.ts b/contracts/integrations/test/exchange-proxy/mtx_test.ts deleted file mode 100644 index 98a3d287e6..0000000000 --- a/contracts/integrations/test/exchange-proxy/mtx_test.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { IExchangeContract } from '@0x/contracts-exchange'; -import { blockchainTests, constants, expect, getRandomPortion, verifyEventsFromLogs } from '@0x/contracts-test-utils'; -import { - artifacts as exchangeProxyArtifacts, - IZeroExContract, - LogMetadataTransformerContract, -} from '@0x/contracts-zero-ex'; -import { migrateOnceAsync } from '@0x/migrations'; -import { assetDataUtils, signatureUtils, SignedExchangeProxyMetaTransaction } from '@0x/order-utils'; -import { - encodeFillQuoteTransformerData, - encodePayTakerTransformerData, - ETH_TOKEN_ADDRESS, - FillQuoteTransformerSide, - findTransformerNonce, - Signature, -} from '@0x/protocol-utils'; -import { AssetProxyId, Order, SignedOrder } from '@0x/types'; -import { BigNumber, hexUtils } from '@0x/utils'; -import * as ethjs from 'ethereumjs-util'; - -const { MAX_UINT256, NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants; - -function sigstruct(signature: string): Signature { - return { - v: parseInt(hexUtils.slice(signature, 0, 1), 16), - signatureType: parseInt(hexUtils.slice(signature, 65, 66), 16), - r: hexUtils.slice(signature, 1, 33), - s: hexUtils.slice(signature, 33, 65), - }; -} - -blockchainTests.resets('exchange proxy - meta-transactions', env => { - const quoteSignerKey = hexUtils.random(); - const quoteSigner = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(quoteSignerKey))); - let owner: string; - let relayer: string; - let maker: string; - let taker: string; - let flashWalletAddress: string; - let zeroEx: IZeroExContract; - let exchange: IExchangeContract; - let inputToken: DummyERC20TokenContract; - let outputToken: DummyERC20TokenContract; - let feeToken: DummyERC20TokenContract; - let addresses: ContractAddresses; - let protocolFee: BigNumber; - let metadataTransformer: LogMetadataTransformerContract; - const GAS_PRICE = new BigNumber('1e9'); - const MAKER_BALANCE = new BigNumber('100e18'); - const TAKER_BALANCE = new BigNumber('100e18'); - const TAKER_FEE_BALANCE = new BigNumber('100e18'); - - before(async () => { - [, relayer, maker, taker] = await env.getAccountAddressesAsync(); - addresses = await migrateOnceAsync(env.provider); - zeroEx = new IZeroExContract(addresses.exchangeProxy, env.provider, env.txDefaults, { - LogMetadataTransformer: LogMetadataTransformerContract.ABI(), - DummyERC20Token: DummyERC20TokenContract.ABI(), - }); - exchange = new IExchangeContract(addresses.exchange, env.provider, env.txDefaults); - [inputToken, outputToken, feeToken] = await Promise.all( - [...new Array(3)].map(i => - DummyERC20TokenContract.deployFrom0xArtifactAsync( - erc20Artifacts.DummyERC20Token, - env.provider, - env.txDefaults, - {}, - `DummyToken-${i}`, - `TOK${i}`, - new BigNumber(18), - BigNumber.max(MAKER_BALANCE, TAKER_BALANCE), - ), - ), - ); - // LogMetadataTransformer is not deployed in migrations. - metadataTransformer = await LogMetadataTransformerContract.deployFrom0xArtifactAsync( - exchangeProxyArtifacts.LogMetadataTransformer, - env.provider, - { - ...env.txDefaults, - from: addresses.exchangeProxyTransformerDeployer, - }, - {}, - ); - owner = await zeroEx.owner().callAsync(); - protocolFee = await exchange.protocolFeeMultiplier().callAsync(); - flashWalletAddress = await zeroEx.getTransformWallet().callAsync(); - const erc20Proxy = await exchange.getAssetProxy(AssetProxyId.ERC20).callAsync(); - const allowanceTarget = await zeroEx.getAllowanceTarget().callAsync(); - await outputToken.mint(MAKER_BALANCE).awaitTransactionSuccessAsync({ from: maker }); - await inputToken.mint(TAKER_BALANCE).awaitTransactionSuccessAsync({ from: taker }); - await feeToken.mint(TAKER_FEE_BALANCE).awaitTransactionSuccessAsync({ from: taker }); - await outputToken.approve(erc20Proxy, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }); - await inputToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }); - await feeToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }); - await zeroEx.setQuoteSigner(quoteSigner).awaitTransactionSuccessAsync({ from: owner }); - }); - - interface Transformation { - deploymentNonce: number; - data: string; - } - - interface SwapInfo { - inputTokenAddress: string; - outputTokenAddress: string; - inputTokenAmount: BigNumber; - minOutputTokenAmount: BigNumber; - transformations: Transformation[]; - orders: SignedOrder[]; - } - - async function generateSwapAsync(orderFields: Partial = {}, isRfqt: boolean = false): Promise { - const order = await signatureUtils.ecSignTypedDataOrderAsync( - env.provider, - { - chainId: 1337, - exchangeAddress: exchange.address, - expirationTimeSeconds: new BigNumber(Date.now()), - salt: new BigNumber(hexUtils.random()), - feeRecipientAddress: NULL_ADDRESS, - senderAddress: NULL_ADDRESS, - takerAddress: isRfqt ? flashWalletAddress : NULL_ADDRESS, - makerAddress: maker, - makerAssetData: assetDataUtils.encodeERC20AssetData(outputToken.address), - takerAssetData: assetDataUtils.encodeERC20AssetData(inputToken.address), - makerFeeAssetData: NULL_BYTES, - takerFeeAssetData: NULL_BYTES, - takerAssetAmount: getRandomPortion(TAKER_BALANCE), - makerAssetAmount: getRandomPortion(MAKER_BALANCE), - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, - ...orderFields, - }, - maker, - ); - const transformations = [ - { - deploymentNonce: findTransformerNonce( - addresses.transformers.fillQuoteTransformer, - addresses.exchangeProxyTransformerDeployer, - ), - data: encodeFillQuoteTransformerData({ - orders: [order], - signatures: [order.signature], - buyToken: outputToken.address, - sellToken: inputToken.address, - fillAmount: order.takerAssetAmount, - maxOrderFillAmounts: [], - refundReceiver: hexUtils.leftPad(2, 20), // Send refund to sender. - rfqtTakerAddress: isRfqt ? taker : NULL_ADDRESS, - side: FillQuoteTransformerSide.Sell, - }), - }, - { - deploymentNonce: findTransformerNonce( - addresses.transformers.payTakerTransformer, - addresses.exchangeProxyTransformerDeployer, - ), - data: encodePayTakerTransformerData({ - tokens: [inputToken.address, outputToken.address, ETH_TOKEN_ADDRESS], - amounts: [MAX_UINT256, MAX_UINT256, MAX_UINT256], - }), - }, - { - deploymentNonce: findTransformerNonce( - metadataTransformer.address, - addresses.exchangeProxyTransformerDeployer, - ), - data: NULL_BYTES, - }, - ]; - return { - transformations, - orders: [order], - inputTokenAddress: inputToken.address, - outputTokenAddress: outputToken.address, - inputTokenAmount: order.takerAssetAmount, - minOutputTokenAmount: order.makerAssetAmount, - }; - } - - function getSwapData(swap: SwapInfo): string { - return zeroEx - .transformERC20( - swap.inputTokenAddress, - swap.outputTokenAddress, - swap.inputTokenAmount, - swap.minOutputTokenAmount, - swap.transformations, - ) - .getABIEncodedTransactionData(); - } - - async function createMetaTransactionAsync( - data: string, - value: BigNumber, - fee?: BigNumber | number, - ): Promise { - return signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync( - env.provider, - { - value, - signer: taker, - sender: relayer, - minGasPrice: GAS_PRICE, - maxGasPrice: GAS_PRICE, - expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / 1000) + 60), - salt: new BigNumber(hexUtils.random()), - callData: data, - feeToken: feeToken.address, - feeAmount: fee !== undefined ? new BigNumber(fee) : getRandomPortion(TAKER_FEE_BALANCE), - domain: { - chainId: 1, - name: 'ZeroEx', - version: '1.0.0', - verifyingContract: zeroEx.address, - }, - }, - taker, - ); - } - - it('can call `transformERC20()` with calldata and no relayer fee', async () => { - const swap = await generateSwapAsync(); - const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed. - const mtx = await createMetaTransactionAsync(getSwapData(swap), _protocolFee, 0); - const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer); - const receipt = await zeroEx - .executeMetaTransaction(mtx, sigstruct(mtx.signature)) - .awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE }); - const relayerEthRefund = relayerEthBalanceBefore - .minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer)) - .minus(GAS_PRICE.times(receipt.gasUsed)); - // Ensure the relayer got back the unused protocol fees. - expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE)); - // Ensure the relayer got paid no mtx fees. - expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0); - // Ensure the taker got output tokens. - expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount); - // Ensure the maker got input tokens. - expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount); - // Check events. - verifyEventsFromLogs( - receipt.logs, - [ - { - taker, - sender: zeroEx.address, - data: NULL_BYTES, - }, - ], - 'TransformerMetadata', - ); - }); - - it('can call `transformERC20()` with calldata and a relayer fee', async () => { - const swap = await generateSwapAsync(); - const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed. - const mtx = await createMetaTransactionAsync(getSwapData(swap), _protocolFee); - const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer); - const receipt = await zeroEx - .executeMetaTransaction(mtx, sigstruct(mtx.signature)) - .awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE }); - const relayerEthRefund = relayerEthBalanceBefore - .minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer)) - .minus(GAS_PRICE.times(receipt.gasUsed)); - // Ensure the relayer got back the unused protocol fees. - expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE)); - // Ensure the relayer got paid mtx fees. - expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(mtx.feeAmount); - // Ensure the taker got output tokens. - expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount); - // Ensure the maker got input tokens. - expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount); - // Check events. - verifyEventsFromLogs( - receipt.logs, - [ - { - taker, - sender: zeroEx.address, - data: NULL_BYTES, - }, - ], - 'TransformerMetadata', - ); - }); - - it('`transformERC20()` can fill RFQT order', async () => { - const swap = await generateSwapAsync({}, true); - const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed. - const mtx = await createMetaTransactionAsync(getSwapData(swap), _protocolFee, 0); - const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer); - const receipt = await zeroEx - .executeMetaTransaction(mtx, sigstruct(mtx.signature)) - .awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE }); - const relayerEthRefund = relayerEthBalanceBefore - .minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer)) - .minus(GAS_PRICE.times(receipt.gasUsed)); - // Ensure the relayer got back the unused protocol fees. - expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE)); - // Ensure the relayer got paid no mtx fees. - expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0); - // Ensure the taker got output tokens. - expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount); - // Ensure the maker got input tokens. - expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount); - // Check events. - verifyEventsFromLogs( - receipt.logs, - [ - { - taker, - sender: zeroEx.address, - data: NULL_BYTES, - }, - ], - 'TransformerMetadata', - ); - }); - - it('`transformERC20()` can fill RFQT order if quote signer configured', async () => { - const swap = await generateSwapAsync({}, true); - const callData = getSwapData(swap); - const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed. - const mtx = await createMetaTransactionAsync(callData, _protocolFee, 0); - const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer); - await zeroEx.setQuoteSigner(NULL_ADDRESS).awaitTransactionSuccessAsync({ from: owner }); - const receipt = await zeroEx - .executeMetaTransaction(mtx, sigstruct(mtx.signature)) - .awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE }); - const relayerEthRefund = relayerEthBalanceBefore - .minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer)) - .minus(GAS_PRICE.times(receipt.gasUsed)); - // Ensure the relayer got back the unused protocol fees. - expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE)); - // Ensure the relayer got paid no mtx fees. - expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0); - // Ensure the taker got output tokens. - expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount); - // Ensure the maker got input tokens. - expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount); - // Check events. - verifyEventsFromLogs( - receipt.logs, - [ - { - taker, - sender: zeroEx.address, - data: NULL_BYTES, - }, - ], - 'TransformerMetadata', - ); - }); -}); diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 2f8ac16175..a9fe48d9c1 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "0.18.2", + "changes": [ + { + "note": "Update FQT for v4 native orders", + "pr": 104 + } + ] + }, { "version": "0.18.1", "changes": [ diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index 228453cc29..95143f9239 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -22,13 +22,12 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; import "../errors/LibTransformERC20RichErrors.sol"; -import "../vendor/v3/IExchange.sol"; -import "../vendor/v3/LibOrderHash.sol"; +import "../features/INativeOrdersFeature.sol"; +import "../features/libs/LibNativeOrder.sol"; import "./bridges/IBridgeAdapter.sol"; import "./Transformer.sol"; import "./LibERC20Transformer.sol"; @@ -41,8 +40,8 @@ contract FillQuoteTransformer is using LibERC20TokenV06 for IERC20TokenV06; using LibERC20Transformer for IERC20TokenV06; using LibSafeMathV06 for uint256; + using LibSafeMathV06 for uint128; using LibRichErrorsV06 for bytes; - using LibBytesV06 for bytes; /// @dev Whether we are performing a market sell or buy. enum Side { @@ -50,6 +49,26 @@ contract FillQuoteTransformer is Buy } + enum OrderType { + Bridge, + Limit, + Rfq + } + + struct LimitOrderInfo { + LibNativeOrder.LimitOrder order; + LibSignature.Signature signature; + // Maximum taker token amount of this limit order to fill. + uint256 maxTakerTokenFillAmount; + } + + struct RfqOrderInfo { + LibNativeOrder.RfqOrder order; + LibSignature.Signature signature; + // Maximum taker token amount of this limit order to fill. + uint256 maxTakerTokenFillAmount; + } + /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { // Whether we are performing a market sell or buy. @@ -60,31 +79,34 @@ contract FillQuoteTransformer is // The token being bought. // This should be an actual token, not the ETH pseudo-token. IERC20TokenV06 buyToken; - // The orders to fill. - IExchange.Order[] orders; - // Signatures for each respective order in `orders`. - bytes[] signatures; - // Maximum fill amount for each order. This may be shorter than the - // number of orders, where missing entries will be treated as `uint256(-1)`. - // For sells, this will be the maximum sell amount (taker asset). - // For buys, this will be the maximum buy amount (maker asset). - uint256[] maxOrderFillAmounts; + + // External liquidity bridge orders. Sorted by fill sequence. + IBridgeAdapter.BridgeOrder[] bridgeOrders; + // Native limit orders. Sorted by fill sequence. + LimitOrderInfo[] limitOrders; + // Native RFQ orders. Sorted by fill sequence. + RfqOrderInfo[] rfqOrders; + + // The sequence to fill the orders in. Each item will fill the next + // order of that type in either `bridgeOrders`, `limitOrders`, + // or `rfqOrders.` + OrderType[] fillSequence; + // Amount of `sellToken` to sell or `buyToken` to buy. - // For sells, this may be `uint256(-1)` to sell the entire balance of - // `sellToken`. + // For sells, setting the high-bit indicates that + // `sellAmount & LOW_BITS` should be treated as a `1e18` fraction of + // the current balance of `sellToken`, where + // `1e18+ == 100%` and `0.5e18 == 50%`, etc. uint256 fillAmount; + // Who to transfer unused protocol fees to. // May be a valid address or one of: // `address(0)`: Stay in flash wallet. // `address(1)`: Send to the taker. // `address(2)`: Send to the sender (caller of `transformERC20()`). address payable refundReceiver; - // Required taker address for RFQT orders. - // Null means any taker can fill it. - address rfqtTakerAddress; } - /// @dev Results of a call to `_fillOrder()`. struct FillOrderResults { // The amount of taker tokens sold, according to balance checks. uint256 takerTokenSoldAmount; @@ -101,7 +123,8 @@ contract FillQuoteTransformer is uint256 soldAmount; uint256 protocolFee; uint256 takerTokenBalanceRemaining; - bool isRfqtAllowed; + uint256[3] currentIndices; + OrderType currentOrderType; } /// @dev Emitted when a trade is skipped due to a lack of funds @@ -109,12 +132,12 @@ contract FillQuoteTransformer is /// @param orderHash The hash of the order that was skipped. event ProtocolFeeUnfunded(bytes32 orderHash); - /// @dev The Exchange ERC20Proxy ID. - bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0; - /// @dev The Exchange ERC20BridgeProxy ID. - bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3; /// @dev Maximum uint256 value. uint256 private constant MAX_UINT256 = uint256(-1); + /// @dev The highest bit of a uint256 value. + uint256 private constant HIGH_BIT = 2 ** 255; + /// @dev Mask of the lower 255 bits of a uint256 value. + uint256 private constant LOWER_255_BITS = HIGH_BIT - 1; /// @dev If `refundReceiver` is set to this address, unpsent /// protocol fees will be sent to the taker. address private constant REFUND_RECEIVER_TAKER = address(1); @@ -122,33 +145,32 @@ contract FillQuoteTransformer is /// protocol fees will be sent to the sender. address private constant REFUND_RECEIVER_SENDER = address(2); - /// @dev The Exchange contract. - IExchange public immutable exchange; - /// @dev The ERC20Proxy address. - address public immutable erc20Proxy; /// @dev The BridgeAdapter address IBridgeAdapter public immutable bridgeAdapter; + /// @dev The exchange proxy contract. + INativeOrdersFeature public immutable zeroEx; + /// @dev Create this contract. - /// @param exchange_ The Exchange V3 instance. - constructor(IExchange exchange_, IBridgeAdapter bridgeAdapter_) + /// @param bridgeAdapter_ The bridge adapter contract. + /// @param zeroEx_ The Exchange Proxy contract. + constructor(IBridgeAdapter bridgeAdapter_, INativeOrdersFeature zeroEx_) public Transformer() { - exchange = exchange_; - erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID); bridgeAdapter = bridgeAdapter_; + zeroEx = zeroEx_; } /// @dev Sell this contract's entire balance of of `sellToken` in exchange /// for `buyToken` by filling `orders`. Protocol fees should be attached /// to this call. `buyToken` and excess ETH will be transferred back to the caller. /// @param context Context information. - /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). + /// @return magicBytes The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). function transform(TransformContext calldata context) external override - returns (bytes4 success) + returns (bytes4 magicBytes) { TransformData memory data = abi.decode(context.data, (TransformData)); FillState memory state; @@ -160,7 +182,11 @@ contract FillQuoteTransformer is context.data ).rrevert(); } - if (data.orders.length != data.signatures.length) { + + if (data.bridgeOrders.length + + data.limitOrders.length + + data.rfqOrders.length != data.fillSequence.length + ) { LibTransformERC20RichErrors.InvalidTransformDataError( LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_ARRAY_LENGTH, context.data @@ -168,76 +194,58 @@ contract FillQuoteTransformer is } state.takerTokenBalanceRemaining = data.sellToken.getTokenBalanceOf(address(this)); - if (data.side == Side.Sell && data.fillAmount == MAX_UINT256) { - // If `sellAmount == -1 then we are selling - // the entire balance of `sellToken`. This is useful in cases where - // the exact sell amount is not exactly known in advance, like when - // unwrapping Chai/cUSDC/cDAI. - data.fillAmount = state.takerTokenBalanceRemaining; + if (data.side == Side.Sell) { + data.fillAmount = _normalizeFillAmount(data.fillAmount, state.takerTokenBalanceRemaining); } - // Approve the ERC20 proxy to spend `sellToken`. - data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount); + // Approve the exchange proxy to spend our sell tokens if native orders + // are present. + if (data.limitOrders.length + data.rfqOrders.length != 0) { + data.sellToken.approveIfBelow(address(zeroEx), data.fillAmount); + // Compute the protocol fee if a limit order is present. + if (data.limitOrders.length != 0) { + state.protocolFee = uint256(zeroEx.getProtocolFeeMultiplier()) + .safeMul(tx.gasprice); + } + } - state.protocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); state.ethRemaining = address(this).balance; - // RFQT orders can only be filled if the actual taker matches the RFQT - // taker (if set). - state.isRfqtAllowed = data.rfqtTakerAddress == address(0) - || context.taker == data.rfqtTakerAddress; // Fill the orders. - for (uint256 i = 0; i < data.orders.length; ++i) { + for (uint256 i = 0; i < data.fillSequence.length; ++i) { // Check if we've hit our targets. if (data.side == Side.Sell) { // Market sell check. - if (state.soldAmount >= data.fillAmount) { - break; - } + if (state.soldAmount >= data.fillAmount) { break; } } else { // Market buy check. - if (state.boughtAmount >= data.fillAmount) { - break; - } + if (state.boughtAmount >= data.fillAmount) { break; } } + state.currentOrderType = OrderType(data.fillSequence[i]); + uint256 orderIndex = state.currentIndices[uint256(state.currentOrderType)]; // Fill the order. FillOrderResults memory results; - if (data.side == Side.Sell) { - // Market sell. - results = _sellToOrder( - data.buyToken, - data.sellToken, - data.orders[i], - data.signatures[i], - data.fillAmount.safeSub(state.soldAmount).min256( - data.maxOrderFillAmounts.length > i - ? data.maxOrderFillAmounts[i] - : MAX_UINT256 - ), - state - ); + if (state.currentOrderType == OrderType.Bridge) { + results = _fillBridgeOrder(data.bridgeOrders[orderIndex], data, state); + } else if (state.currentOrderType == OrderType.Limit) { + results = _fillLimitOrder(data.limitOrders[orderIndex], data, state); + } else if (state.currentOrderType == OrderType.Rfq) { + results = _fillRfqOrder(data.rfqOrders[orderIndex], data, state); } else { - // Market buy. - results = _buyFromOrder( - data.buyToken, - data.sellToken, - data.orders[i], - data.signatures[i], - data.fillAmount.safeSub(state.boughtAmount).min256( - data.maxOrderFillAmounts.length > i - ? data.maxOrderFillAmounts[i] - : MAX_UINT256 - ), - state - ); + revert("INVALID_ORDER_TYPE"); } // Accumulate totals. - state.soldAmount = state.soldAmount.safeAdd(results.takerTokenSoldAmount); - state.boughtAmount = state.boughtAmount.safeAdd(results.makerTokenBoughtAmount); - state.ethRemaining = state.ethRemaining.safeSub(results.protocolFeePaid); - state.takerTokenBalanceRemaining = state.takerTokenBalanceRemaining.safeSub(results.takerTokenSoldAmount); + state.soldAmount = state.soldAmount + .safeAdd(results.takerTokenSoldAmount); + state.boughtAmount = state.boughtAmount + .safeAdd(results.makerTokenBoughtAmount); + state.ethRemaining = state.ethRemaining + .safeSub(results.protocolFeePaid); + state.takerTokenBalanceRemaining = state.takerTokenBalanceRemaining + .safeSub(results.takerTokenSoldAmount); + state.currentIndices[uint256(state.currentOrderType)]++; } // Ensure we hit our targets. @@ -276,223 +284,174 @@ contract FillQuoteTransformer is return LibERC20Transformer.TRANSFORMER_SUCCESS; } - /// @dev Try to sell up to `sellAmount` from an order. - /// @param makerToken The maker/buy token. - /// @param takerToken The taker/sell token. - /// @param order The order to fill. - /// @param signature The signature for `order`. - /// @param sellAmount Amount of taker token to sell. - /// @param state Intermediate state variables to get around stack limits. - function _sellToOrder( - IERC20TokenV06 makerToken, - IERC20TokenV06 takerToken, - IExchange.Order memory order, - bytes memory signature, - uint256 sellAmount, + // Fill a single bridge order. + function _fillBridgeOrder( + IBridgeAdapter.BridgeOrder memory order, + TransformData memory data, FillState memory state ) private returns (FillOrderResults memory results) { - IERC20TokenV06 takerFeeToken = - _getTokenFromERC20AssetData(order.takerFeeAssetData); - - uint256 takerTokenFillAmount = sellAmount; - - if (order.takerFee != 0) { - if (takerFeeToken == makerToken) { - // Taker fee is payable in the maker token, so we need to - // approve the proxy to spend the maker token. - // It isn't worth computing the actual taker fee - // since `approveIfBelow()` will set the allowance to infinite. We - // just need a reasonable upper bound to avoid unnecessarily re-approving. - takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee); - } else if (takerFeeToken == takerToken){ - // Taker fee is payable in the taker token, so we need to - // reduce the fill amount to cover the fee. - // takerTokenFillAmount' = - // (takerTokenFillAmount * order.takerAssetAmount) / - // (order.takerAssetAmount + order.takerFee) - takerTokenFillAmount = LibMathV06.getPartialAmountCeil( - order.takerAssetAmount, - order.takerAssetAmount.safeAdd(order.takerFee), - sellAmount - ); - } else { - // Only support taker or maker asset denominated taker fees. - LibTransformERC20RichErrors.InvalidTakerFeeTokenError( - address(takerFeeToken) - ).rrevert(); - } - } - - // Perform the fill. - return _fillOrder( - order, - signature, - takerTokenFillAmount, + uint256 takerTokenFillAmount = _computeTakerTokenFillAmount( + data, state, - takerFeeToken == takerToken + order.takerTokenAmount, + order.makerTokenAmount, + 0 ); + + (bool success, bytes memory resultData) = address(bridgeAdapter).delegatecall( + abi.encodeWithSelector( + IBridgeAdapter.trade.selector, + order, + data.sellToken, + data.buyToken, + takerTokenFillAmount + ) + ); + if (success) { + results.makerTokenBoughtAmount = abi.decode(resultData, (uint256)); + results.takerTokenSoldAmount = takerTokenFillAmount; + } } - /// @dev Try to buy up to `buyAmount` from an order. - /// @param makerToken The maker/buy token. - /// @param takerToken The taker/sell token. - /// @param order The order to fill. - /// @param signature The signature for `order`. - /// @param buyAmount Amount of maker token to buy. - /// @param state Intermediate state variables to get around stack limits. - function _buyFromOrder( - IERC20TokenV06 makerToken, - IERC20TokenV06 takerToken, - IExchange.Order memory order, - bytes memory signature, - uint256 buyAmount, + // Fill a single limit order. + function _fillLimitOrder( + LimitOrderInfo memory orderInfo, + TransformData memory data, FillState memory state ) private returns (FillOrderResults memory results) { - IERC20TokenV06 takerFeeToken = - _getTokenFromERC20AssetData(order.takerFeeAssetData); - // Compute the default taker token fill amount. - uint256 takerTokenFillAmount = LibMathV06.getPartialAmountCeil( - buyAmount, - order.makerAssetAmount, - order.takerAssetAmount + uint256 takerTokenFillAmount = LibSafeMathV06.min256( + _computeTakerTokenFillAmount( + data, + state, + orderInfo.order.takerAmount, + orderInfo.order.makerAmount, + orderInfo.order.takerTokenFeeAmount + ), + orderInfo.maxTakerTokenFillAmount ); - if (order.takerFee != 0) { - if (takerFeeToken == makerToken) { - // Taker fee is payable in the maker token. - // Adjust the taker token fill amount to account for maker - // tokens being lost to the taker fee. - // takerTokenFillAmount' = - // (order.takerAssetAmount * buyAmount) / - // (order.makerAssetAmount - order.takerFee) - takerTokenFillAmount = LibMathV06.getPartialAmountCeil( - buyAmount, - order.makerAssetAmount.safeSub(order.takerFee), - order.takerAssetAmount - ); - // Approve the proxy to spend the maker token. - // It isn't worth computing the actual taker fee - // since `approveIfBelow()` will set the allowance to infinite. We - // just need a reasonable upper bound to avoid unnecessarily re-approving. - takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee); - } else if (takerFeeToken != takerToken) { - // Only support taker or maker asset denominated taker fees. - LibTransformERC20RichErrors.InvalidTakerFeeTokenError( - address(takerFeeToken) - ).rrevert(); - } + // Emit an event if we do not have sufficient ETH to cover the protocol fee. + if (state.ethRemaining < state.protocolFee) { + bytes32 orderHash = zeroEx.getLimitOrderHash(orderInfo.order); + emit ProtocolFeeUnfunded(orderHash); + return results; // Empty results. } - // Perform the fill. - return _fillOrder( - order, - signature, - takerTokenFillAmount, - state, - takerFeeToken == takerToken - ); - } - - /// @dev Attempt to fill an order. If the fill reverts, the revert will be - /// swallowed and `results` will be zeroed out. - /// @param order The order to fill. - /// @param signature The order signature. - /// @param takerAssetFillAmount How much taker asset to fill. - /// @param state Intermediate state variables to get around stack limits. - /// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the - /// taker token. - function _fillOrder( - IExchange.Order memory order, - bytes memory signature, - uint256 takerAssetFillAmount, - FillState memory state, - bool isTakerFeeInTakerToken - ) - private - returns (FillOrderResults memory results) - { - // Clamp to remaining taker asset amount or order size. - uint256 availableTakerAssetFillAmount = - takerAssetFillAmount.min256(order.takerAssetAmount); - availableTakerAssetFillAmount = - availableTakerAssetFillAmount.min256(state.takerTokenBalanceRemaining); - - // If it is a Bridge order we fill this directly through the BridgeAdapter - if (order.makerAssetData.readBytes4(0) == ERC20_BRIDGE_PROXY_ID) { - (bool success, bytes memory resultData) = address(bridgeAdapter).delegatecall( - abi.encodeWithSelector( - IBridgeAdapter.trade.selector, - order.makerAssetData, - address(_getTokenFromERC20AssetData(order.takerAssetData)), - availableTakerAssetFillAmount + try + zeroEx.fillLimitOrder + {value: state.protocolFee} + ( + orderInfo.order, + orderInfo.signature, + takerTokenFillAmount.safeDowncastToUint128() ) - ); - if (success) { - results.makerTokenBoughtAmount = abi.decode(resultData, (uint256)); - results.takerTokenSoldAmount = availableTakerAssetFillAmount; - // protocol fee paid remains 0 - } - return results; - } else { - // If the order taker address is set to this contract's address then - // this is an RFQT order, and we will only fill it if allowed to. - if (order.takerAddress == address(this) && !state.isRfqtAllowed) { - return results; // Empty results. - } - // Emit an event if we do not have sufficient ETH to cover the protocol fee. - if (state.ethRemaining < state.protocolFee) { - bytes32 orderHash = LibOrderHash.getTypedDataHash( - order, - exchange.EIP712_EXCHANGE_DOMAIN_HASH() + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + if (orderInfo.order.takerTokenFeeAmount > 0) { + takerTokenFilledAmount = takerTokenFilledAmount.safeAdd128( + LibMathV06.getPartialAmountFloor( + takerTokenFilledAmount, + orderInfo.order.takerAmount, + orderInfo.order.takerTokenFeeAmount + ).safeDowncastToUint128() ); - emit ProtocolFeeUnfunded(orderHash); - return results; } - try - exchange.fillOrder - {value: state.protocolFee} - (order, availableTakerAssetFillAmount, signature) - returns (IExchange.FillResults memory fillResults) - { - results.makerTokenBoughtAmount = fillResults.makerAssetFilledAmount; - results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount; - results.protocolFeePaid = fillResults.protocolFeePaid; - // If the taker fee is payable in the taker asset, include the - // taker fee in the total amount sold. - if (isTakerFeeInTakerToken) { - results.takerTokenSoldAmount = - results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid); - } - } catch (bytes memory) { - // Swallow failures, leaving all results as zero. - } - } + results.takerTokenSoldAmount = takerTokenFilledAmount; + results.makerTokenBoughtAmount = makerTokenFilledAmount; + results.protocolFeePaid = state.protocolFee; + } catch {} } - /// @dev Extract the token from plain ERC20 asset data. - /// If the asset-data is empty, a zero token address will be returned. - /// @param assetData The order asset data. - function _getTokenFromERC20AssetData(bytes memory assetData) + // Fill a single RFQ order. + function _fillRfqOrder( + RfqOrderInfo memory orderInfo, + TransformData memory data, + FillState memory state + ) + private + returns (FillOrderResults memory results) + { + uint256 takerTokenFillAmount = LibSafeMathV06.min256( + _computeTakerTokenFillAmount( + data, + state, + orderInfo.order.takerAmount, + orderInfo.order.makerAmount, + 0 + ), + orderInfo.maxTakerTokenFillAmount + ); + + try + zeroEx.fillRfqOrder + ( + orderInfo.order, + orderInfo.signature, + takerTokenFillAmount.safeDowncastToUint128() + ) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + results.takerTokenSoldAmount = takerTokenFilledAmount; + results.makerTokenBoughtAmount = makerTokenFilledAmount; + } catch {} + } + + // Compute the next taker token fill amount of a generic order. + function _computeTakerTokenFillAmount( + TransformData memory data, + FillState memory state, + uint256 orderTakerAmount, + uint256 orderMakerAmount, + uint256 orderTakerTokenFeeAmount + ) private pure - returns (IERC20TokenV06 token) + returns (uint256 takerTokenFillAmount) { - if (assetData.length == 0) { - return IERC20TokenV06(address(0)); + if (data.side == Side.Sell) { + takerTokenFillAmount = data.fillAmount.safeSub(state.soldAmount); + if (orderTakerTokenFeeAmount != 0) { + takerTokenFillAmount = LibMathV06.getPartialAmountCeil( + takerTokenFillAmount, + orderTakerAmount.safeAdd(orderTakerTokenFeeAmount), + orderTakerAmount + ); + } + } else { // Buy + takerTokenFillAmount = LibMathV06.getPartialAmountCeil( + data.fillAmount.safeSub(state.boughtAmount), + orderMakerAmount, + orderTakerAmount + ); } - if (assetData.length != 36 || - LibBytesV06.readBytes4(assetData, 0) != ERC20_ASSET_PROXY_ID) - { - LibTransformERC20RichErrors - .InvalidERC20AssetDataError(assetData) - .rrevert(); + return LibSafeMathV06.min256( + LibSafeMathV06.min256(takerTokenFillAmount, orderTakerAmount), + state.takerTokenBalanceRemaining + ); + } + + // Convert possible proportional values to absolute quantities. + function _normalizeFillAmount(uint256 rawAmount, uint256 balance) + private + pure + returns (uint256 normalized) + { + if ((rawAmount & HIGH_BIT) == HIGH_BIT) { + // If the high bit of `rawAmount` is set then the lower 255 bits + // specify a fraction of `balance`. + return LibSafeMathV06.min256( + balance + * LibSafeMathV06.min256(rawAmount & LOWER_255_BITS, 1e18) + / 1e18, + balance + ); } - return IERC20TokenV06(LibBytesV06.readAddress(assetData, 16)); + return rawAmount; } } diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol index 59c662014b..7cfc1c33da 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol @@ -20,7 +20,8 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -import "./mixins/MixinAdapterAddresses.sol"; +import "./IBridgeAdapter.sol"; +import "./BridgeSource.sol"; import "./mixins/MixinBalancer.sol"; import "./mixins/MixinBancor.sol"; import "./mixins/MixinCoFiX.sol"; @@ -38,7 +39,7 @@ import "./mixins/MixinUniswapV2.sol"; import "./mixins/MixinZeroExBridge.sol"; contract BridgeAdapter is - MixinAdapterAddresses, + IBridgeAdapter, MixinBalancer, MixinBancor, MixinCoFiX, @@ -55,203 +56,146 @@ contract BridgeAdapter is MixinUniswapV2, MixinZeroExBridge { - - /// @dev Emitted when a trade occurs. - /// @param inputToken The token the bridge is converting from. - /// @param outputToken The token the bridge is converting to. - /// @param inputTokenAmount Amount of input token. - /// @param outputTokenAmount Amount of output token. - /// @param from The bridge address, indicating the underlying source of the fill. - /// @param to The `to` address, currrently `address(this)` - event ERC20BridgeTransfer( - IERC20TokenV06 inputToken, - IERC20TokenV06 outputToken, - uint256 inputTokenAmount, - uint256 outputTokenAmount, - address from, - address to - ); - - address private immutable BALANCER_BRIDGE_ADDRESS; - address private immutable BANCOR_BRIDGE_ADDRESS; - address private immutable COFIX_BRIDGE_ADDRESS; - address private immutable CREAM_BRIDGE_ADDRESS; - address private immutable CURVE_BRIDGE_ADDRESS; - address private immutable CRYPTO_COM_BRIDGE_ADDRESS; - address private immutable DODO_BRIDGE_ADDRESS; - address private immutable KYBER_BRIDGE_ADDRESS; - address private immutable MOONISWAP_BRIDGE_ADDRESS; - address private immutable MSTABLE_BRIDGE_ADDRESS; - address private immutable OASIS_BRIDGE_ADDRESS; - address private immutable SHELL_BRIDGE_ADDRESS; - address private immutable SNOW_SWAP_BRIDGE_ADDRESS; - address private immutable SUSHISWAP_BRIDGE_ADDRESS; - address private immutable SWERVE_BRIDGE_ADDRESS; - address private immutable UNISWAP_BRIDGE_ADDRESS; - address private immutable UNISWAP_V2_BRIDGE_ADDRESS; - - constructor(AdapterAddresses memory addresses) + constructor(IEtherTokenV06 weth) public MixinBalancer() - MixinBancor(addresses) + MixinBancor(weth) MixinCoFiX() MixinCurve() MixinCryptoCom() - MixinDodo(addresses) - MixinKyber(addresses) - MixinMooniswap(addresses) - MixinMStable(addresses) - MixinOasis(addresses) + MixinDodo() + MixinKyber(weth) + MixinMooniswap(weth) + MixinMStable() + MixinOasis() MixinShell() - MixinSushiswap(addresses) - MixinUniswap(addresses) - MixinUniswapV2(addresses) + MixinSushiswap() + MixinUniswap(weth) + MixinUniswapV2() MixinZeroExBridge() - { - BALANCER_BRIDGE_ADDRESS = addresses.balancerBridge; - BANCOR_BRIDGE_ADDRESS = addresses.bancorBridge; - COFIX_BRIDGE_ADDRESS = addresses.cofixBridge; - CURVE_BRIDGE_ADDRESS = addresses.curveBridge; - CRYPTO_COM_BRIDGE_ADDRESS = addresses.cryptoComBridge; - KYBER_BRIDGE_ADDRESS = addresses.kyberBridge; - MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge; - MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge; - OASIS_BRIDGE_ADDRESS = addresses.oasisBridge; - SHELL_BRIDGE_ADDRESS = addresses.shellBridge; - SUSHISWAP_BRIDGE_ADDRESS = addresses.sushiswapBridge; - SWERVE_BRIDGE_ADDRESS = addresses.swerveBridge; - UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge; - UNISWAP_V2_BRIDGE_ADDRESS = addresses.uniswapV2Bridge; - CREAM_BRIDGE_ADDRESS = addresses.creamBridge; - SNOW_SWAP_BRIDGE_ADDRESS = addresses.snowSwapBridge; - DODO_BRIDGE_ADDRESS = addresses.dodoBridge; - } + {} function trade( - bytes calldata makerAssetData, + BridgeOrder memory order, IERC20TokenV06 sellToken, + IERC20TokenV06 buyToken, uint256 sellAmount ) - external + public + override returns (uint256 boughtAmount) { - ( - IERC20TokenV06 buyToken, - address bridgeAddress, - bytes memory bridgeData - ) = abi.decode( - makerAssetData[4:], - (IERC20TokenV06, address, bytes) - ); - require( - bridgeAddress != address(this) && bridgeAddress != address(0), - "BridgeAdapter/INVALID_BRIDGE_ADDRESS" - ); - - if (bridgeAddress == CURVE_BRIDGE_ADDRESS || - bridgeAddress == SWERVE_BRIDGE_ADDRESS || - bridgeAddress == SNOW_SWAP_BRIDGE_ADDRESS) { + if (order.source == BridgeSource.CURVE || + order.source == BridgeSource.SWERVE || + order.source == BridgeSource.SNOWSWAP) { boughtAmount = _tradeCurve( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == SUSHISWAP_BRIDGE_ADDRESS) { - boughtAmount = _tradeSushiswap( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == UNISWAP_V2_BRIDGE_ADDRESS) { - boughtAmount = _tradeUniswapV2( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == UNISWAP_BRIDGE_ADDRESS) { - boughtAmount = _tradeUniswap( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == BALANCER_BRIDGE_ADDRESS || - bridgeAddress == CREAM_BRIDGE_ADDRESS) { - boughtAmount = _tradeBalancer( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == KYBER_BRIDGE_ADDRESS) { - boughtAmount = _tradeKyber( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == MOONISWAP_BRIDGE_ADDRESS) { - boughtAmount = _tradeMooniswap( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == MSTABLE_BRIDGE_ADDRESS) { - boughtAmount = _tradeMStable( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == OASIS_BRIDGE_ADDRESS) { - boughtAmount = _tradeOasis( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == SHELL_BRIDGE_ADDRESS) { - boughtAmount = _tradeShell( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == DODO_BRIDGE_ADDRESS) { - boughtAmount = _tradeDodo( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == CRYPTO_COM_BRIDGE_ADDRESS) { - boughtAmount = _tradeCryptoCom( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == BANCOR_BRIDGE_ADDRESS) { - boughtAmount = _tradeBancor( - buyToken, - sellAmount, - bridgeData - ); - } else if (bridgeAddress == COFIX_BRIDGE_ADDRESS) { - boughtAmount = _tradeCoFiX( - buyToken, - sellAmount, - bridgeData - ); - } else { - boughtAmount = _tradeZeroExBridge( - bridgeAddress, sellToken, buyToken, sellAmount, - bridgeData + order.bridgeData + ); + } else if (order.source == BridgeSource.SUSHISWAP) { + boughtAmount = _tradeSushiswap( + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.UNISWAPV2) { + boughtAmount = _tradeUniswapV2( + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.UNISWAP) { + boughtAmount = _tradeUniswap( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.BALANCER || + order.source == BridgeSource.CREAM) { + boughtAmount = _tradeBalancer( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.KYBER) { + boughtAmount = _tradeKyber( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.MOONISWAP) { + boughtAmount = _tradeMooniswap( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.MSTABLE) { + boughtAmount = _tradeMStable( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.OASIS) { + boughtAmount = _tradeOasis( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.SHELL) { + boughtAmount = _tradeShell( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.DODO) { + boughtAmount = _tradeDodo( + sellToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.CRYPTOCOM) { + boughtAmount = _tradeCryptoCom( + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.BANCOR) { + boughtAmount = _tradeBancor( + buyToken, + sellAmount, + order.bridgeData + ); + } else if (order.source == BridgeSource.COFIX) { + boughtAmount = _tradeCoFiX( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else { + boughtAmount = _tradeZeroExBridge( + sellToken, + buyToken, + sellAmount, + order.bridgeData ); } - emit ERC20BridgeTransfer( + emit BridgeFill( + order.source, sellToken, buyToken, sellAmount, - boughtAmount, - bridgeAddress, - address(this) + boughtAmount ); } } diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeSource.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeSource.sol new file mode 100644 index 0000000000..40b1734640 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeSource.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + + +library BridgeSource { + uint256 constant internal BALANCER = 0; + uint256 constant internal BANCOR = 1; + uint256 constant internal COFIX = 2; + uint256 constant internal CURVE = 3; + uint256 constant internal CREAM = 4; + uint256 constant internal CRYPTOCOM = 5; + uint256 constant internal DODO = 6; + uint256 constant internal KYBER = 7; + uint256 constant internal LIQUIDITYPROVIDER = 8; + uint256 constant internal MOONISWAP = 9; + uint256 constant internal MSTABLE = 10; + uint256 constant internal OASIS = 11; + uint256 constant internal SHELL = 12; + uint256 constant internal SNOWSWAP = 13; + uint256 constant internal SUSHISWAP = 14; + uint256 constant internal SWERVE = 15; + uint256 constant internal UNISWAP = 16; + uint256 constant internal UNISWAPV2 = 17; + // New sources should be APPENDED to this list, taking the next highest + // integer value. +} diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/IBridgeAdapter.sol b/contracts/zero-ex/contracts/src/transformers/bridges/IBridgeAdapter.sol index 8a7578c336..d83d11ada8 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/IBridgeAdapter.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/IBridgeAdapter.sol @@ -18,12 +18,38 @@ */ pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; + interface IBridgeAdapter { + struct BridgeOrder { + uint256 source; + uint256 takerTokenAmount; + uint256 makerTokenAmount; + bytes bridgeData; + } + + /// @dev Emitted when tokens are swapped with an external source. + /// @param source The unique ID for the source. See `BridgeSource.sol` + /// @param inputToken The token the bridge is converting from. + /// @param outputToken The token the bridge is converting to. + /// @param inputTokenAmount Amount of input token sold. + /// @param outputTokenAmount Amount of output token bought. + event BridgeFill( + uint256 source, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount + ); + function trade( - bytes calldata makerAssetData, - address fromTokenAddress, + BridgeOrder calldata order, + IERC20TokenV06 sellToken, + IERC20TokenV06 buyToken, uint256 sellAmount ) external diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol deleted file mode 100644 index 4f5189ab98..0000000000 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6.5; - -contract MixinAdapterAddresses -{ - - struct AdapterAddresses { - // Bridges - address balancerBridge; - address bancorBridge; - address cofixBridge; - address creamBridge; - address curveBridge; - address cryptoComBridge; - address dodoBridge; - address kyberBridge; - address mooniswapBridge; - address mStableBridge; - address oasisBridge; - address shellBridge; - address snowSwapBridge; - address swerveBridge; - address sushiswapBridge; - address uniswapBridge; - address uniswapV2Bridge; - // Exchanges - address kyberNetworkProxy; - address oasis; - address sushiswapRouter; - address uniswapV2Router; - address uniswapExchangeFactory; - address mStable; - address dodoHelper; - // Other - address weth; - } -} diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancer.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancer.sol index b07dec50d5..f404f5298e 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancer.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancer.sol @@ -46,6 +46,7 @@ contract MixinBalancer { using LibERC20TokenV06 for IERC20TokenV06; function _tradeBalancer( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -54,9 +55,9 @@ contract MixinBalancer { returns (uint256 boughtAmount) { // Decode the bridge data. - (IERC20TokenV06 sellToken, IBalancerPool pool) = abi.decode( + (IBalancerPool pool) = abi.decode( bridgeData, - (IERC20TokenV06, IBalancerPool) + (IBalancerPool) ); sellToken.approveIfBelow( address(pool), diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBancor.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBancor.sol index fe79e01db4..8c85611be2 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBancor.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBancor.sol @@ -24,12 +24,12 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; interface IBancorNetwork { function convertByPath( - address[] calldata _path, + IERC20TokenV06[] calldata _path, uint256 _amount, uint256 _minReturn, address _beneficiary, @@ -42,17 +42,17 @@ interface IBancorNetwork { } -contract MixinBancor is - MixinAdapterAddresses -{ +contract MixinBancor { + /// @dev Bancor ETH pseudo-address. - address constant public BANCOR_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + IERC20TokenV06 constant public BANCOR_ETH_ADDRESS = + IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); IEtherTokenV06 private immutable WETH; - constructor(AdapterAddresses memory addresses) + constructor(IEtherTokenV06 weth) public { - WETH = IEtherTokenV06(addresses.weth); + WETH = weth; } function _tradeBancor( @@ -64,17 +64,22 @@ contract MixinBancor is returns (uint256 boughtAmount) { // Decode the bridge data. - ( - address[] memory path, - address bancorNetworkAddress - // solhint-disable indent - ) = abi.decode(bridgeData, (address[], address)); - // solhint-enable indent + IBancorNetwork bancorNetworkAddress; + IERC20TokenV06[] memory path; + { + address[] memory _path; + ( + bancorNetworkAddress, + _path + ) = abi.decode(bridgeData, (IBancorNetwork, address[])); + // To get around `abi.decode()` not supporting interface array types. + assembly { path := _path } + } require(path.length >= 2, "MixinBancor/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); require( - path[path.length - 1] == address(buyToken) || - (path[path.length - 1] == BANCOR_ETH_ADDRESS && address(buyToken) == address(WETH)), + path[path.length - 1] == buyToken || + (path[path.length - 1] == BANCOR_ETH_ADDRESS && buyToken == WETH), "MixinBancor/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" ); @@ -88,14 +93,14 @@ contract MixinBancor is } else { // Grant an allowance to the Bancor Network. LibERC20TokenV06.approveIfBelow( - IERC20TokenV06(path[0]), - bancorNetworkAddress, + path[0], + address(bancorNetworkAddress), sellAmount ); } // Convert the tokens - boughtAmount = IBancorNetwork(bancorNetworkAddress).convertByPath{value: payableAmount}( + boughtAmount = bancorNetworkAddress.convertByPath{value: payableAmount}( path, // path originating with source token and terminating in destination token sellAmount, // amount of source token to trade 1, // minimum amount of destination token expected to receive diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCoFiX.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCoFiX.sol index 5bd9b4dae4..65a8e84b06 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCoFiX.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCoFiX.sol @@ -23,7 +23,6 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; -import "./MixinAdapterAddresses.sol"; interface ICoFiXRouter { @@ -53,15 +52,20 @@ interface ICoFiXPair { function swapWithExact(address outToken, address to) external payable - returns (uint amountIn, uint amountOut, uint oracleFeeChange, uint256[4] memory tradeInfo); + returns ( + uint amountIn, + uint amountOut, + uint oracleFeeChange, + uint256[4] memory tradeInfo + ); } -contract MixinCoFiX is - MixinAdapterAddresses -{ +contract MixinCoFiX { + using LibERC20TokenV06 for IERC20TokenV06; function _tradeCoFiX( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -69,15 +73,16 @@ contract MixinCoFiX is internal returns (uint256 boughtAmount) { - (address fromTokenAddress, uint256 fee, address pool) = abi.decode(bridgeData, (address, uint256, address)); + (uint256 fee, ICoFiXPair pool) = abi.decode(bridgeData, (uint256, ICoFiXPair)); // Transfer tokens into the pool LibERC20TokenV06.compatTransfer( - IERC20TokenV06(fromTokenAddress), - pool, - sellAmount); + sellToken, + address(pool), + sellAmount + ); // Call the swap exact with the tokens now in the pool // pay the NEST Oracle fee with ETH - (/* In */, boughtAmount, , ) = ICoFiXPair(pool).swapWithExact{value: fee}( + (/* In */, boughtAmount, , ) = pool.swapWithExact{value: fee}( address(buyToken), address(this) ); diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCryptoCom.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCryptoCom.sol index 39a9d4c083..06dc461306 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCryptoCom.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCryptoCom.sol @@ -37,21 +37,24 @@ contract MixinCryptoCom internal returns (uint256 boughtAmount) { - // solhint-disable indent - address[] memory path; - address router; - (path, router) = abi.decode(bridgeData, (address[], address)); - // solhint-enable indent + IUniswapV2Router02 router; + IERC20TokenV06[] memory path; + { + address[] memory _path; + (router, _path) = abi.decode(bridgeData, (IUniswapV2Router02, address[])); + // To get around `abi.decode()` not supporting interface array types. + assembly { path := _path } + } - require(path.length >= 2, "CryptoComBridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); + require(path.length >= 2, "MixinCryptoCom/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); require( - path[path.length - 1] == address(buyToken), - "CryptoComBridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" + path[path.length - 1] == buyToken, + "MixinCryptoCom/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" ); // Grant the CryptoCom router an allowance to sell the first token. - IERC20TokenV06(path[0]).approveIfBelow(router, sellAmount); + path[0].approveIfBelow(address(router), sellAmount); - uint[] memory amounts = IUniswapV2Router02(router).swapExactTokensForTokens( + uint[] memory amounts = router.swapExactTokensForTokens( // Sell all tokens we hold. sellAmount, // Minimum buy amount. diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCurve.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCurve.sol index 844161306f..a16188a03f 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCurve.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCurve.sol @@ -35,12 +35,12 @@ contract MixinCurve { struct CurveBridgeData { address curveAddress; bytes4 exchangeFunctionSelector; - IERC20TokenV06 sellToken; int128 fromCoinIdx; int128 toCoinIdx; } function _tradeCurve( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -50,7 +50,7 @@ contract MixinCurve { { // Decode the bridge data to get the Curve metadata. CurveBridgeData memory data = abi.decode(bridgeData, (CurveBridgeData)); - data.sellToken.approveIfBelow(data.curveAddress, sellAmount); + sellToken.approveIfBelow(data.curveAddress, sellAmount); uint256 beforeBalance = buyToken.balanceOf(address(this)); (bool success, bytes memory resultData) = data.curveAddress.call(abi.encodeWithSelector( diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinDodo.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinDodo.sol index 985e04837f..f4a6bdd5aa 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinDodo.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinDodo.sol @@ -23,55 +23,60 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./MixinAdapterAddresses.sol"; - -interface IDODOHelper { - - function querySellQuoteToken(address dodo, uint256 amount) external view returns (uint256); -} +import "../IBridgeAdapter.sol"; interface IDODO { + function sellBaseToken( + uint256 amount, + uint256 minReceiveQuote, + bytes calldata data + ) + external + returns (uint256); - function sellBaseToken(uint256 amount, uint256 minReceiveQuote, bytes calldata data) external returns (uint256); - - function buyBaseToken(uint256 amount, uint256 maxPayQuote, bytes calldata data) external returns (uint256); - + function buyBaseToken( + uint256 amount, + uint256 maxPayQuote, + bytes calldata data + ) + external + returns (uint256); } -contract MixinDodo is - MixinAdapterAddresses -{ +interface IDODOHelper { + function querySellQuoteToken( + IDODO dodo, + uint256 amount + ) + external + view + returns (uint256); +} + + +contract MixinDodo { + using LibERC20TokenV06 for IERC20TokenV06; - /// @dev Mainnet address of the `DOODO Helper` contract. - IDODOHelper private immutable DODO_HELPER; - - constructor(AdapterAddresses memory addresses) - public - { - DODO_HELPER = IDODOHelper(addresses.dodoHelper); - } - function _tradeDodo( - IERC20TokenV06 /* buyToken */, + IERC20TokenV06 sellToken, uint256 sellAmount, bytes memory bridgeData ) internal returns (uint256 boughtAmount) { - (address fromTokenAddress, - address pool, - bool isSellBase) = abi.decode(bridgeData, (address, address, bool)); + (IDODOHelper helper, IDODO pool, bool isSellBase) = + abi.decode(bridgeData, (IDODOHelper, IDODO, bool)); // Grant the Dodo pool contract an allowance to sell the first token. - IERC20TokenV06(fromTokenAddress).approveIfBelow(pool, sellAmount); + sellToken.approveIfBelow(address(pool), sellAmount); if (isSellBase) { // Sell the Base token directly against the contract - boughtAmount = IDODO(pool).sellBaseToken( + boughtAmount = pool.sellBaseToken( // amount to sell sellAmount, // min receive amount @@ -80,11 +85,11 @@ contract MixinDodo is ); } else { // Need to re-calculate the sell quote amount into buyBase - boughtAmount = DODO_HELPER.querySellQuoteToken( + boughtAmount = helper.querySellQuoteToken( pool, sellAmount ); - IDODO(pool).buyBaseToken( + pool.buyBaseToken( // amount to buy boughtAmount, // max pay amount diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinKyber.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinKyber.sol index 2085140c61..03bd7df6ab 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinKyber.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinKyber.sol @@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; interface IKyberNetworkProxy { @@ -54,26 +54,24 @@ interface IKyberNetworkProxy { returns (uint256 boughtAmount); } -contract MixinKyber is - MixinAdapterAddresses -{ +contract MixinKyber { + using LibERC20TokenV06 for IERC20TokenV06; /// @dev Address indicating the trade is using ETH - address private immutable KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + IERC20TokenV06 private immutable KYBER_ETH_ADDRESS = + IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); /// @dev Mainnet address of the WETH contract. IEtherTokenV06 private immutable WETH; - /// @dev Mainnet address of the KyberNetworkProxy contract. - IKyberNetworkProxy private immutable KYBER_NETWORK_PROXY; - constructor(AdapterAddresses memory addresses) + constructor(IEtherTokenV06 weth) public { - WETH = IEtherTokenV06(addresses.weth); - KYBER_NETWORK_PROXY = IKyberNetworkProxy(addresses.kyberNetworkProxy); + WETH = weth; } function _tradeKyber( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -81,15 +79,15 @@ contract MixinKyber is internal returns (uint256 boughtAmount) { - (IERC20TokenV06 sellToken, bytes memory hint) = - abi.decode(bridgeData, (IERC20TokenV06, bytes)); + (IKyberNetworkProxy kyber, bytes memory hint) = + abi.decode(bridgeData, (IKyberNetworkProxy, bytes)); uint256 payableAmount = 0; if (sellToken != WETH) { // If the input token is not WETH, grant an allowance to the exchange // to spend them. sellToken.approveIfBelow( - address(KYBER_NETWORK_PROXY), + address(kyber), sellAmount ); } else { @@ -100,13 +98,13 @@ contract MixinKyber is // Try to sell all of this contract's input token balance through // `KyberNetworkProxy.trade()`. - boughtAmount = KYBER_NETWORK_PROXY.tradeWithHint{ value: payableAmount }( + boughtAmount = kyber.tradeWithHint{ value: payableAmount }( // Input token. - sellToken == WETH ? IERC20TokenV06(KYBER_ETH_ADDRESS) : sellToken, + sellToken == WETH ? KYBER_ETH_ADDRESS : sellToken, // Sell amount. sellAmount, // Output token. - buyToken == WETH ? IERC20TokenV06(KYBER_ETH_ADDRESS) : buyToken, + buyToken == WETH ? KYBER_ETH_ADDRESS : buyToken, // Transfer to this contract address(uint160(address(this))), // Buy as much as possible. diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMStable.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMStable.sol index 5c56670ffe..6691026006 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMStable.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMStable.sol @@ -22,7 +22,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; interface IMStable { @@ -37,21 +37,12 @@ interface IMStable { returns (uint256 boughtAmount); } -contract MixinMStable is - MixinAdapterAddresses -{ +contract MixinMStable { + using LibERC20TokenV06 for IERC20TokenV06; - /// @dev Mainnet address of the mStable mUSD contract. - IMStable private immutable MSTABLE; - - constructor(AdapterAddresses memory addresses) - public - { - MSTABLE = IMStable(addresses.mStable); - } - function _tradeMStable( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -59,12 +50,12 @@ contract MixinMStable is internal returns (uint256 boughtAmount) { - // Decode the bridge data to get the `sellToken`. - (IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06)); - // Grant an allowance to the exchange to spend `sellToken` token. - sellToken.approveIfBelow(address(MSTABLE), sellAmount); + (IMStable mstable) = abi.decode(bridgeData, (IMStable)); - boughtAmount = MSTABLE.swap( + // Grant an allowance to the exchange to spend `sellToken` token. + sellToken.approveIfBelow(address(mstable), sellAmount); + + boughtAmount = mstable.swap( sellToken, buyToken, sellAmount, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMooniswap.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMooniswap.sol index 8b8eea81a7..571b5b40ad 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMooniswap.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinMooniswap.sol @@ -24,7 +24,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; /// @dev Moooniswap pool interface. @@ -43,22 +43,22 @@ interface IMooniswapPool { } /// @dev BridgeAdapter mixin for mooniswap. -contract MixinMooniswap is - MixinAdapterAddresses -{ +contract MixinMooniswap { + using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IEtherTokenV06; /// @dev WETH token. IEtherTokenV06 private immutable WETH; - constructor(AdapterAddresses memory addresses) + constructor(IEtherTokenV06 weth) public { - WETH = IEtherTokenV06(addresses.weth); + WETH = weth; } function _tradeMooniswap( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -66,8 +66,7 @@ contract MixinMooniswap is internal returns (uint256 boughtAmount) { - (IERC20TokenV06 sellToken, IMooniswapPool pool) = - abi.decode(bridgeData, (IERC20TokenV06, IMooniswapPool)); + (IMooniswapPool pool) = abi.decode(bridgeData, (IMooniswapPool)); // Convert WETH to ETH. uint256 ethValue = 0; diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinOasis.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinOasis.sol index b1817ea2bf..544b750156 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinOasis.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinOasis.sol @@ -22,7 +22,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; interface IOasis { @@ -42,21 +42,12 @@ interface IOasis { returns (uint256 boughtAmount); } -contract MixinOasis is - MixinAdapterAddresses -{ +contract MixinOasis { + using LibERC20TokenV06 for IERC20TokenV06; - /// @dev Mainnet address of the Oasis `MatchingMarket` contract. - IOasis private immutable OASIS; - - constructor(AdapterAddresses memory addresses) - public - { - OASIS = IOasis(addresses.oasis); - } - function _tradeOasis( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -64,15 +55,16 @@ contract MixinOasis is internal returns (uint256 boughtAmount) { - // Decode the bridge data to get the `sellToken`. - (IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06)); + + (IOasis oasis) = abi.decode(bridgeData, (IOasis)); + // Grant an allowance to the exchange to spend `sellToken` token. sellToken.approveIfBelow( - address(OASIS), + address(oasis), sellAmount ); // Try to sell all of this contract's `sellToken` token balance. - boughtAmount = OASIS.sellAllAmount( + boughtAmount = oasis.sellAllAmount( sellToken, sellAmount, buyToken, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol index e2e1040e55..4ddb957ce1 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol @@ -23,13 +23,12 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./MixinAdapterAddresses.sol"; interface IShell { function originSwap( - address from, - address to, + IERC20TokenV06 from, + IERC20TokenV06 to, uint256 fromAmount, uint256 minTargetAmount, uint256 deadline @@ -38,12 +37,12 @@ interface IShell { returns (uint256 toAmount); } -contract MixinShell is - MixinAdapterAddresses -{ +contract MixinShell { + using LibERC20TokenV06 for IERC20TokenV06; function _tradeShell( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -51,17 +50,17 @@ contract MixinShell is internal returns (uint256 boughtAmount) { - (address fromTokenAddress, address pool) = abi.decode(bridgeData, (address, address)); + IShell pool = abi.decode(bridgeData, (IShell)); // Grant the Shell contract an allowance to sell the first token. - IERC20TokenV06(fromTokenAddress).approveIfBelow( - pool, + IERC20TokenV06(sellToken).approveIfBelow( + address(pool), sellAmount ); - boughtAmount = IShell(pool).originSwap( - fromTokenAddress, - address(buyToken), + boughtAmount = pool.originSwap( + sellToken, + buyToken, // Sell all tokens we hold. sellAmount, // Minimum buy amount. diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinSushiswap.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinSushiswap.sol index 06b1acba73..1fc8f6a682 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinSushiswap.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinSushiswap.sol @@ -23,23 +23,12 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./MixinAdapterAddresses.sol"; import "./MixinUniswapV2.sol"; -contract MixinSushiswap is - MixinAdapterAddresses -{ +contract MixinSushiswap { + using LibERC20TokenV06 for IERC20TokenV06; - /// @dev Mainnet address of the `SushiswapRouter` contract. - IUniswapV2Router02 private immutable SUSHISWAP_ROUTER; - - constructor(AdapterAddresses memory addresses) - public - { - SUSHISWAP_ROUTER = IUniswapV2Router02(addresses.sushiswapRouter); - } - function _tradeSushiswap( IERC20TokenV06 buyToken, uint256 sellAmount, @@ -48,22 +37,28 @@ contract MixinSushiswap is internal returns (uint256 boughtAmount) { - // solhint-disable indent - address[] memory path = abi.decode(bridgeData, (address[])); - // solhint-enable indent + IERC20TokenV06[] memory path; + IUniswapV2Router02 router; + { + address[] memory _path; + (router, _path) = + abi.decode(bridgeData, (IUniswapV2Router02, address[])); + // To get around `abi.decode()` not supporting interface array types. + assembly { path := _path } + } - require(path.length >= 2, "SushiswapBridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); + require(path.length >= 2, "MixinSushiswap/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); require( - path[path.length - 1] == address(buyToken), - "SushiswapBridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" + path[path.length - 1] == buyToken, + "MixinSushiswap/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" ); // Grant the Uniswap router an allowance to sell the first token. - IERC20TokenV06(path[0]).approveIfBelow( - address(SUSHISWAP_ROUTER), + path[0].approveIfBelow( + address(router), sellAmount ); - uint[] memory amounts = SUSHISWAP_ROUTER.swapExactTokensForTokens( + uint[] memory amounts = router.swapExactTokensForTokens( // Sell all tokens we hold. sellAmount, // Minimum buy amount. diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswap.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswap.sol index 67068c1ace..d1d15744e6 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswap.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswap.sol @@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; interface IUniswapExchangeFactory { @@ -103,24 +103,21 @@ interface IUniswapExchange { returns (uint256 tokensBought); } -contract MixinUniswap is - MixinAdapterAddresses -{ +contract MixinUniswap { + using LibERC20TokenV06 for IERC20TokenV06; /// @dev Mainnet address of the WETH contract. IEtherTokenV06 private immutable WETH; - /// @dev Mainnet address of the `UniswapExchangeFactory` contract. - IUniswapExchangeFactory private immutable UNISWAP_EXCHANGE_FACTORY; - constructor(AdapterAddresses memory addresses) + constructor(IEtherTokenV06 weth) public { - WETH = IEtherTokenV06(addresses.weth); - UNISWAP_EXCHANGE_FACTORY = IUniswapExchangeFactory(addresses.uniswapExchangeFactory); + WETH = weth; } function _tradeUniswap( + IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, bytes memory bridgeData @@ -128,11 +125,12 @@ contract MixinUniswap is internal returns (uint256 boughtAmount) { - // Decode the bridge data to get the `sellToken`. - (IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06)); + IUniswapExchangeFactory exchangeFactory = + abi.decode(bridgeData, (IUniswapExchangeFactory)); // Get the exchange for the token pair. IUniswapExchange exchange = _getUniswapExchangeForTokenPair( + exchangeFactory, sellToken, buyToken ); @@ -197,10 +195,12 @@ contract MixinUniswap is /// @dev Retrieves the uniswap exchange for a given token pair. /// In the case of a WETH-token exchange, this will be the non-WETH token. /// In th ecase of a token-token exchange, this will be the first token. + /// @param exchangeFactory The exchange factory. /// @param sellToken The address of the token we are converting from. /// @param buyToken The address of the token we are converting to. /// @return exchange The uniswap exchange. function _getUniswapExchangeForTokenPair( + IUniswapExchangeFactory exchangeFactory, IERC20TokenV06 sellToken, IERC20TokenV06 buyToken ) @@ -210,8 +210,8 @@ contract MixinUniswap is { // Whichever isn't WETH is the exchange token. exchange = sellToken == WETH - ? UNISWAP_EXCHANGE_FACTORY.getExchange(buyToken) - : UNISWAP_EXCHANGE_FACTORY.getExchange(sellToken); - require(address(exchange) != address(0), "NO_UNISWAP_EXCHANGE_FOR_TOKEN"); + ? exchangeFactory.getExchange(buyToken) + : exchangeFactory.getExchange(sellToken); + require(address(exchange) != address(0), "MixinUniswap/NO_EXCHANGE"); } } diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswapV2.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswapV2.sol index 7ae28bc422..f0ff35f707 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswapV2.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinUniswapV2.sol @@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./MixinAdapterAddresses.sol"; +import "../IBridgeAdapter.sol"; /* UniswapV2 @@ -42,26 +42,16 @@ interface IUniswapV2Router02 { function swapExactTokensForTokens( uint amountIn, uint amountOutMin, - address[] calldata path, + IERC20TokenV06[] calldata path, address to, uint deadline ) external returns (uint[] memory amounts); } -contract MixinUniswapV2 is - MixinAdapterAddresses -{ +contract MixinUniswapV2 { + using LibERC20TokenV06 for IERC20TokenV06; - /// @dev Mainnet address of the `UniswapV2Router02` contract. - IUniswapV2Router02 private immutable UNISWAP_V2_ROUTER; - - constructor(AdapterAddresses memory addresses) - public - { - UNISWAP_V2_ROUTER = IUniswapV2Router02(addresses.uniswapV2Router); - } - function _tradeUniswapV2( IERC20TokenV06 buyToken, uint256 sellAmount, @@ -70,22 +60,24 @@ contract MixinUniswapV2 is internal returns (uint256 boughtAmount) { - // solhint-disable indent - address[] memory path = abi.decode(bridgeData, (address[])); - // solhint-enable indent + IUniswapV2Router02 router; + IERC20TokenV06[] memory path; + { + address[] memory _path; + (router, _path) = abi.decode(bridgeData, (IUniswapV2Router02, address[])); + // To get around `abi.decode()` not supporting interface array types. + assembly { path := _path } + } - require(path.length >= 2, "UniswapV2Bridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); + require(path.length >= 2, "MixinUniswapV3/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); require( - path[path.length - 1] == address(buyToken), - "UniswapV2Bridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" + path[path.length - 1] == buyToken, + "MixinUniswapV2/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN" ); // Grant the Uniswap router an allowance to sell the first token. - IERC20TokenV06(path[0]).approveIfBelow( - address(UNISWAP_V2_ROUTER), - sellAmount - ); + path[0].approveIfBelow(address(router), sellAmount); - uint[] memory amounts = UNISWAP_V2_ROUTER.swapExactTokensForTokens( + uint[] memory amounts = router.swapExactTokensForTokens( // Sell all tokens we hold. sellAmount, // Minimum buy amount. diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol index 00918f860a..7903266079 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol @@ -31,7 +31,6 @@ contract MixinZeroExBridge { using LibSafeMathV06 for uint256; function _tradeZeroExBridge( - address bridgeAddress, IERC20TokenV06 sellToken, IERC20TokenV06 buyToken, uint256 sellAmount, @@ -40,17 +39,19 @@ contract MixinZeroExBridge { internal returns (uint256 boughtAmount) { + (ILiquidityProvider provider, bytes memory lpData) = + abi.decode(bridgeData, (ILiquidityProvider, bytes)); // Trade the good old fashioned way sellToken.compatTransfer( - bridgeAddress, + address(provider), sellAmount ); - boughtAmount = ILiquidityProvider(bridgeAddress).sellTokenForToken( + boughtAmount = provider.sellTokenForToken( address(sellToken), address(buyToken), address(this), // recipient 1, // minBuyAmount - bridgeData + lpData ); } } diff --git a/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol b/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol deleted file mode 100644 index 3061d151d8..0000000000 --- a/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6.5; -pragma experimental ABIEncoderV2; - - -/// @dev Interface to the V3 Exchange. -interface IExchange { - - /// @dev V3 Order structure. - struct Order { - // Address that created the order. - address makerAddress; - // Address that is allowed to fill the order. - // If set to 0, any address is allowed to fill the order. - address takerAddress; - // Address that will recieve fees when order is filled. - address feeRecipientAddress; - // Address that is allowed to call Exchange contract methods that affect this order. - // If set to 0, any address is allowed to call these methods. - address senderAddress; - // Amount of makerAsset being offered by maker. Must be greater than 0. - uint256 makerAssetAmount; - // Amount of takerAsset being bid on by maker. Must be greater than 0. - uint256 takerAssetAmount; - // Fee paid to feeRecipient by maker when order is filled. - uint256 makerFee; - // Fee paid to feeRecipient by taker when order is filled. - uint256 takerFee; - // Timestamp in seconds at which order expires. - uint256 expirationTimeSeconds; - // Arbitrary number to facilitate uniqueness of the order's hash. - uint256 salt; - // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. - // The leading bytes4 references the id of the asset proxy. - bytes makerAssetData; - // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. - // The leading bytes4 references the id of the asset proxy. - bytes takerAssetData; - // Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset. - // The leading bytes4 references the id of the asset proxy. - bytes makerFeeAssetData; - // Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset. - // The leading bytes4 references the id of the asset proxy. - bytes takerFeeAssetData; - } - - /// @dev V3 `fillOrder()` results.` - struct FillResults { - // Total amount of makerAsset(s) filled. - uint256 makerAssetFilledAmount; - // Total amount of takerAsset(s) filled. - uint256 takerAssetFilledAmount; - // Total amount of fees paid by maker(s) to feeRecipient(s). - uint256 makerFeePaid; - // Total amount of fees paid by taker to feeRecipients(s). - uint256 takerFeePaid; - // Total amount of fees paid by taker to the staking contract. - uint256 protocolFeePaid; - } - - /// @dev Fills the input order. - /// @param order Order struct containing order specifications. - /// @param takerAssetFillAmount Desired amount of takerAsset to sell. - /// @param signature Proof that order has been created by maker. - /// @return fillResults Amounts filled and fees paid by maker and taker. - function fillOrder( - Order calldata order, - uint256 takerAssetFillAmount, - bytes calldata signature - ) - external - payable - returns (FillResults memory fillResults); - - /// @dev Returns the protocolFeeMultiplier - /// @return multiplier The multiplier for protocol fees. - function protocolFeeMultiplier() - external - view - returns (uint256 multiplier); - - /// @dev Gets an asset proxy. - /// @param assetProxyId Id of the asset proxy. - /// @return proxyAddress The asset proxy registered to assetProxyId. - /// Returns 0x0 if no proxy is registered. - function getAssetProxy(bytes4 assetProxyId) - external - view - returns (address proxyAddress); - - function EIP712_EXCHANGE_DOMAIN_HASH() - external - view - returns (bytes32 domainHash); -} diff --git a/contracts/zero-ex/contracts/src/vendor/v3/IGasToken.sol b/contracts/zero-ex/contracts/src/vendor/v3/IGasToken.sol deleted file mode 100644 index 5267fdd222..0000000000 --- a/contracts/zero-ex/contracts/src/vendor/v3/IGasToken.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6.5; - -interface IGasToken { - - /// @dev Frees up to `value` sub-tokens - /// @param value The amount of tokens to free - /// @return freed How many tokens were freed - function freeUpTo(uint256 value) external returns (uint256 freed); - - /// @dev Frees up to `value` sub-tokens owned by `from` - /// @param from The owner of tokens to spend - /// @param value The amount of tokens to free - /// @return freed How many tokens were freed - function freeFromUpTo(address from, uint256 value) external returns (uint256 freed); - - /// @dev Mints `value` amount of tokens - /// @param value The amount of tokens to mint - function mint(uint256 value) external; -} diff --git a/contracts/zero-ex/contracts/src/vendor/v3/LibOrderHash.sol b/contracts/zero-ex/contracts/src/vendor/v3/LibOrderHash.sol deleted file mode 100644 index 7e827212f8..0000000000 --- a/contracts/zero-ex/contracts/src/vendor/v3/LibOrderHash.sol +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6.5; - -import "./IExchange.sol"; - - -library LibOrderHash { - - using LibOrderHash for IExchange.Order; - - // Hash for the EIP712 Order Schema: - // keccak256(abi.encodePacked( - // "Order(", - // "address makerAddress,", - // "address takerAddress,", - // "address feeRecipientAddress,", - // "address senderAddress,", - // "uint256 makerAssetAmount,", - // "uint256 takerAssetAmount,", - // "uint256 makerFee,", - // "uint256 takerFee,", - // "uint256 expirationTimeSeconds,", - // "uint256 salt,", - // "bytes makerAssetData,", - // "bytes takerAssetData,", - // "bytes makerFeeAssetData,", - // "bytes takerFeeAssetData", - // ")" - // )) - bytes32 constant internal _EIP712_ORDER_SCHEMA_HASH = - 0xf80322eb8376aafb64eadf8f0d7623f22130fd9491a221e902b713cb984a7534; - - /// @dev Calculates the EIP712 typed data hash of an order with a given domain separator. - /// @param order The order structure. - /// @param eip712ExchangeDomainHash Domain hash for the Exchange. - /// @return orderHash EIP712 typed data hash of the order. - function getTypedDataHash(IExchange.Order memory order, bytes32 eip712ExchangeDomainHash) - internal - pure - returns (bytes32 orderHash) - { - orderHash = _hashEIP712Message( - eip712ExchangeDomainHash, - order.getStructHash() - ); - return orderHash; - } - - /// @dev Calculates EIP712 hash of the order struct. - /// @param order The order structure. - /// @return result EIP712 hash of the order struct. - function getStructHash(IExchange.Order memory order) - internal - pure - returns (bytes32 result) - { - bytes32 schemaHash = _EIP712_ORDER_SCHEMA_HASH; - bytes memory makerAssetData = order.makerAssetData; - bytes memory takerAssetData = order.takerAssetData; - bytes memory makerFeeAssetData = order.makerFeeAssetData; - bytes memory takerFeeAssetData = order.takerFeeAssetData; - - // Assembly for more efficiently computing: - // keccak256(abi.encodePacked( - // EIP712_ORDER_SCHEMA_HASH, - // uint256(order.makerAddress), - // uint256(order.takerAddress), - // uint256(order.feeRecipientAddress), - // uint256(order.senderAddress), - // order.makerAssetAmount, - // order.takerAssetAmount, - // order.makerFee, - // order.takerFee, - // order.expirationTimeSeconds, - // order.salt, - // keccak256(order.makerAssetData), - // keccak256(order.takerAssetData), - // keccak256(order.makerFeeAssetData), - // keccak256(order.takerFeeAssetData) - // )); - - assembly { - // Assert order offset (this is an internal error that should never be triggered) - if lt(order, 32) { - invalid() - } - - // Calculate memory addresses that will be swapped out before hashing - let pos1 := sub(order, 32) - let pos2 := add(order, 320) - let pos3 := add(order, 352) - let pos4 := add(order, 384) - let pos5 := add(order, 416) - - // Backup - let temp1 := mload(pos1) - let temp2 := mload(pos2) - let temp3 := mload(pos3) - let temp4 := mload(pos4) - let temp5 := mload(pos5) - - // Hash in place - mstore(pos1, schemaHash) - mstore(pos2, keccak256(add(makerAssetData, 32), mload(makerAssetData))) // store hash of makerAssetData - mstore(pos3, keccak256(add(takerAssetData, 32), mload(takerAssetData))) // store hash of takerAssetData - mstore(pos4, keccak256(add(makerFeeAssetData, 32), mload(makerFeeAssetData))) // store hash of makerFeeAssetData - mstore(pos5, keccak256(add(takerFeeAssetData, 32), mload(takerFeeAssetData))) // store hash of takerFeeAssetData - result := keccak256(pos1, 480) - - // Restore - mstore(pos1, temp1) - mstore(pos2, temp2) - mstore(pos3, temp3) - mstore(pos4, temp4) - mstore(pos5, temp5) - } - return result; - } - - /// @dev Calculates EIP712 encoding for a hash struct with a given domain hash. - /// @param eip712DomainHash Hash of the domain domain separator data, computed - /// with getDomainHash(). - /// @param hashStruct The EIP712 hash struct. - /// @return result EIP712 hash applied to the given EIP712 Domain. - function _hashEIP712Message(bytes32 eip712DomainHash, bytes32 hashStruct) - internal - pure - returns (bytes32 result) - { - // Assembly for more efficient computing: - // keccak256(abi.encodePacked( - // EIP191_HEADER, - // EIP712_DOMAIN_HASH, - // hashStruct - // )); - - assembly { - // Load free memory pointer - let memPtr := mload(64) - - mstore(memPtr, 0x1901000000000000000000000000000000000000000000000000000000000000) // EIP191 header - mstore(add(memPtr, 2), eip712DomainHash) // EIP712 domain hash - mstore(add(memPtr, 34), hashStruct) // Hash of struct - - // Compute hash - result := keccak256(memPtr, 66) - } - return result; - } -} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol index b1788fc6c7..ab78481ee0 100644 --- a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol @@ -27,39 +27,25 @@ import "./TestMintableERC20Token.sol"; contract TestFillQuoteTransformerBridge { - struct FillBehavior { - // Scaling for maker assets minted, in 1e18. - uint256 makerAssetMintRatio; - uint256 amount; - } + uint256 private constant REVERT_AMOUNT = 0xdeadbeef; function sellTokenForToken( - address takerToken, + address /* takerToken */, address makerToken, address recipient, - uint256 minBuyAmount, + uint256 /* minBuyAmount */, bytes calldata auxiliaryData ) external returns (uint256 boughtAmount) { - FillBehavior memory behavior = abi.decode(auxiliaryData, (FillBehavior)); - boughtAmount = LibMathV06.getPartialAmountFloor( - behavior.makerAssetMintRatio, - 1e18, - behavior.amount - ); + boughtAmount = abi.decode(auxiliaryData, (uint256)); + if (REVERT_AMOUNT == boughtAmount) { + revert("REVERT_AMOUNT"); + } TestMintableERC20Token(makerToken).mint( recipient, boughtAmount ); } - - function encodeBehaviorData(FillBehavior calldata behavior) - external - pure - returns (bytes memory encoded) - { - return abi.encode(behavior); - } } diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol index 93622efa2a..9d13e0dca6 100644 --- a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol @@ -23,106 +23,109 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; -import "../src/vendor/v3/IExchange.sol"; import "./TestMintableERC20Token.sol"; +import "../src/features/libs/LibNativeOrder.sol"; +import "../src/features/libs/LibSignature.sol"; contract TestFillQuoteTransformerExchange { - struct FillBehavior { - // How much of the order is filled, in taker asset amount. - uint256 filledTakerAssetAmount; - // Scaling for maker assets minted, in 1e18. - uint256 makerAssetMintRatio; - } - bytes32 public constant EIP712_EXCHANGE_DOMAIN_HASH = 0xaa81d881b1adbbf115e15b849cb9cdc643cad3c6a90f30eb505954af943247e6; - + uint256 private constant REVERT_AMOUNT = 0xdeadbeef; uint256 private constant PROTOCOL_FEE_MULTIPLIER = 1337; using LibSafeMathV06 for uint256; - function fillOrder( - IExchange.Order calldata order, - uint256 takerAssetFillAmount, - bytes calldata signature + function fillLimitOrder( + LibNativeOrder.LimitOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount ) external payable - returns (IExchange.FillResults memory fillResults) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { - require( - signature.length != 0, - "TestFillQuoteTransformerExchange/INVALID_SIGNATURE" - ); - // The signature is the ABI-encoded FillBehavior data. - FillBehavior memory behavior = abi.decode(signature, (FillBehavior)); - + // The r field of the signature is the pre-filled amount. + uint128 takerTokenPreFilledAmount = uint128(uint256(signature.r)); + if (REVERT_AMOUNT == takerTokenPreFilledAmount) { + revert("REVERT_AMOUNT"); + } + if (takerTokenPreFilledAmount >= order.takerAmount) { + revert('FILLED'); + } uint256 protocolFee = PROTOCOL_FEE_MULTIPLIER * tx.gasprice; - require( - msg.value == protocolFee, - "TestFillQuoteTransformerExchange/INSUFFICIENT_PROTOCOL_FEE" - ); // Return excess protocol fee. msg.sender.transfer(msg.value - protocolFee); + takerTokenFilledAmount = LibSafeMathV06.min128( + order.takerAmount - takerTokenPreFilledAmount, + takerTokenFillAmount + ); // Take taker tokens. - TestMintableERC20Token takerToken = _getTokenFromAssetData(order.takerAssetData); - takerAssetFillAmount = LibSafeMathV06.min256( - order.takerAssetAmount.safeSub(behavior.filledTakerAssetAmount), - takerAssetFillAmount + order.takerToken.transferFrom( + msg.sender, + order.maker, + takerTokenFilledAmount ); - require( - takerToken.getSpendableAmount(msg.sender, address(this)) >= takerAssetFillAmount, - "TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FUNDS" - ); - takerToken.transferFrom(msg.sender, order.makerAddress, takerAssetFillAmount); // Mint maker tokens. - uint256 makerAssetFilledAmount = LibMathV06.getPartialAmountFloor( - takerAssetFillAmount, - order.takerAssetAmount, - order.makerAssetAmount - ); - TestMintableERC20Token makerToken = _getTokenFromAssetData(order.makerAssetData); - makerToken.mint( - msg.sender, - LibMathV06.getPartialAmountFloor( - behavior.makerAssetMintRatio, - 1e18, - makerAssetFilledAmount - ) + makerTokenFilledAmount = LibSafeMathV06.safeDowncastToUint128( + uint256(takerTokenFilledAmount) + * uint256(order.makerAmount) + / uint256(order.takerAmount) ); + TestMintableERC20Token(address(order.makerToken)) + .mint(msg.sender, makerTokenFilledAmount); - // Take taker fee. - TestMintableERC20Token takerFeeToken = _getTokenFromAssetData(order.takerFeeAssetData); - uint256 takerFee = LibMathV06.getPartialAmountFloor( - takerAssetFillAmount, - order.takerAssetAmount, - order.takerFee + // Take taker token fee. + uint128 takerFee = LibSafeMathV06.safeDowncastToUint128( + uint256(takerTokenFilledAmount) + * uint256(order.takerTokenFeeAmount) + / uint256(order.takerAmount) ); - require( - takerFeeToken.getSpendableAmount(msg.sender, address(this)) >= takerFee, - "TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FEE_FUNDS" - ); - takerFeeToken.transferFrom(msg.sender, order.feeRecipientAddress, takerFee); - - fillResults.makerAssetFilledAmount = makerAssetFilledAmount; - fillResults.takerAssetFilledAmount = takerAssetFillAmount; - fillResults.makerFeePaid = uint256(-1); - fillResults.takerFeePaid = takerFee; - fillResults.protocolFeePaid = protocolFee; + order.takerToken.transferFrom(msg.sender, order.feeRecipient, takerFee); } - function encodeBehaviorData(FillBehavior calldata behavior) + function fillRfqOrder( + LibNativeOrder.RfqOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount + ) external - pure - returns (bytes memory encoded) + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { - return abi.encode(behavior); + // The r field of the signature is the pre-filled amount. + uint128 takerTokenPreFilledAmount = uint128(uint256(signature.r)); + if (REVERT_AMOUNT == takerTokenPreFilledAmount) { + revert("REVERT_AMOUNT"); + } + if (takerTokenPreFilledAmount >= order.takerAmount) { + revert('FILLED'); + } + takerTokenFilledAmount = LibSafeMathV06.min128( + order.takerAmount - takerTokenPreFilledAmount, + takerTokenFillAmount + ); + + // Take taker tokens. + order.takerToken.transferFrom( + msg.sender, + order.maker, + takerTokenFilledAmount + ); + + // Mint maker tokens. + makerTokenFilledAmount = LibSafeMathV06.safeDowncastToUint128( + uint256(takerTokenFilledAmount) + * uint256(order.makerAmount) + / uint256(order.takerAmount) + ); + TestMintableERC20Token(address(order.makerToken)) + .mint(msg.sender, makerTokenFilledAmount); } - function protocolFeeMultiplier() + function getProtocolFeeMultiplier() external pure returns (uint256) @@ -130,19 +133,11 @@ contract TestFillQuoteTransformerExchange { return PROTOCOL_FEE_MULTIPLIER; } - function getAssetProxy(bytes4) + function getLimitOrderHash(LibNativeOrder.LimitOrder calldata order) external - view - returns (address) - { - return address(this); - } - - function _getTokenFromAssetData(bytes memory assetData) - private pure - returns (TestMintableERC20Token token) + returns (bytes32) { - return TestMintableERC20Token(LibBytesV06.readAddress(assetData, 16)); + return bytes32(order.salt); } } diff --git a/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol b/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol index 69654c18c6..7f9ab9629d 100644 --- a/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol +++ b/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol @@ -65,14 +65,13 @@ contract TestLiquidityProvider { /// @param outputToken The token being bought. /// @param recipient The recipient of the bought tokens. /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. - /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. /// @return boughtAmount The amount of `outputToken` bought. function sellTokenForToken( address inputToken, address outputToken, address recipient, uint256 minBuyAmount, - bytes calldata auxiliaryData + bytes calldata // auxiliaryData ) external returns (uint256 boughtAmount) @@ -91,13 +90,12 @@ contract TestLiquidityProvider { /// @param outputToken The token being bought. /// @param recipient The recipient of the bought tokens. /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. - /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. /// @return boughtAmount The amount of `outputToken` bought. function sellEthForToken( address outputToken, address recipient, uint256 minBuyAmount, - bytes calldata auxiliaryData + bytes calldata // auxiliaryData ) external returns (uint256 boughtAmount) @@ -115,13 +113,12 @@ contract TestLiquidityProvider { /// @param inputToken The token being sold. /// @param recipient The recipient of the bought tokens. /// @param minBuyAmount The minimum acceptable amount of ETH to buy. - /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. /// @return boughtAmount The amount of ETH bought. function sellTokenForEth( address inputToken, address payable recipient, uint256 minBuyAmount, - bytes calldata auxiliaryData + bytes calldata // auxiliaryData ) external returns (uint256 boughtAmount) diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 95ff28b8a1..cca95f9270 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -43,7 +43,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOrderHash|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index fc487e54d1..9f6270d64a 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -9,6 +9,7 @@ import * as AffiliateFeeTransformer from '../test/generated-artifacts/AffiliateF import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json'; import * as BootstrapFeature from '../test/generated-artifacts/BootstrapFeature.json'; import * as BridgeAdapter from '../test/generated-artifacts/BridgeAdapter.json'; +import * as BridgeSource from '../test/generated-artifacts/BridgeSource.json'; import * as FeeCollector from '../test/generated-artifacts/FeeCollector.json'; import * as FeeCollectorController from '../test/generated-artifacts/FeeCollectorController.json'; import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; @@ -24,10 +25,8 @@ import * as IBootstrapFeature from '../test/generated-artifacts/IBootstrapFeatur import * as IBridgeAdapter from '../test/generated-artifacts/IBridgeAdapter.json'; import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json'; import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json'; -import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; -import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json'; import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json'; @@ -53,7 +52,6 @@ import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json'; import * as LibNativeOrder from '../test/generated-artifacts/LibNativeOrder.json'; import * as LibNativeOrdersRichErrors from '../test/generated-artifacts/LibNativeOrdersRichErrors.json'; import * as LibNativeOrdersStorage from '../test/generated-artifacts/LibNativeOrdersStorage.json'; -import * as LibOrderHash from '../test/generated-artifacts/LibOrderHash.json'; import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRichErrors.json'; import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; @@ -73,7 +71,6 @@ import * as LiquidityProviderFeature from '../test/generated-artifacts/Liquidity import * as LiquidityProviderSandbox from '../test/generated-artifacts/LiquidityProviderSandbox.json'; import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; -import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.json'; import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json'; import * as MixinBancor from '../test/generated-artifacts/MixinBancor.json'; import * as MixinCoFiX from '../test/generated-artifacts/MixinCoFiX.json'; @@ -211,8 +208,8 @@ export const artifacts = { Transformer: Transformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact, BridgeAdapter: BridgeAdapter as ContractArtifact, + BridgeSource: BridgeSource as ContractArtifact, IBridgeAdapter: IBridgeAdapter as ContractArtifact, - MixinAdapterAddresses: MixinAdapterAddresses as ContractArtifact, MixinBalancer: MixinBalancer as ContractArtifact, MixinBancor: MixinBancor as ContractArtifact, MixinCoFiX: MixinCoFiX as ContractArtifact, @@ -230,10 +227,7 @@ export const artifacts = { MixinZeroExBridge: MixinZeroExBridge as ContractArtifact, ILiquidityProvider: ILiquidityProvider as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact, - IExchange: IExchange as ContractArtifact, - IGasToken: IGasToken as ContractArtifact, IStaking: IStaking as ContractArtifact, - LibOrderHash: LibOrderHash as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, TestBridge: TestBridge as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact, diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts index ad618fea87..1d0e2ec4de 100644 --- a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -5,16 +5,28 @@ import { expect, getRandomInteger, Numberish, - randomAddress, } from '@0x/contracts-test-utils'; -import { assetDataUtils } from '@0x/order-utils'; -import { encodeFillQuoteTransformerData, FillQuoteTransformerData, FillQuoteTransformerSide } from '@0x/protocol-utils'; -import { Order } from '@0x/types'; +import { + encodeFillQuoteTransformerData, + FillQuoteTransformerBridgeOrder as BridgeOrder, + FillQuoteTransformerData, + FillQuoteTransformerLimitOrderInfo, + FillQuoteTransformerOrderType as OrderType, + FillQuoteTransformerRfqOrderInfo, + FillQuoteTransformerSide as Side, + LimitOrder, + LimitOrderFields, + RfqOrder, + RfqOrderFields, + Signature, +} from '@0x/protocol-utils'; import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs as TxReceipt } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts } from '../artifacts'; import { TestFillQuoteTransformerBridgeContract } from '../generated-wrappers/test_fill_quote_transformer_bridge'; +import { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders'; import { BridgeAdapterContract, FillQuoteTransformerContract, @@ -40,6 +52,9 @@ blockchainTests.resets('FillQuoteTransformer', env => { let singleProtocolFee: BigNumber; const GAS_PRICE = 1337; + const TEST_BRIDGE_SOURCE = 12345678; + const HIGH_BIT = new BigNumber(2).pow(255); + const REVERT_AMOUNT = new BigNumber('0xdeadbeef'); before(async () => { [maker, feeRecipient, sender, taker] = await env.getAccountAddressesAsync(); @@ -54,41 +69,15 @@ blockchainTests.resets('FillQuoteTransformer', env => { env.provider, env.txDefaults, artifacts, - { - balancerBridge: NULL_ADDRESS, - curveBridge: NULL_ADDRESS, - kyberBridge: NULL_ADDRESS, - mooniswapBridge: NULL_ADDRESS, - mStableBridge: NULL_ADDRESS, - oasisBridge: NULL_ADDRESS, - sushiswapBridge: NULL_ADDRESS, - swerveBridge: NULL_ADDRESS, - uniswapBridge: NULL_ADDRESS, - uniswapV2Bridge: NULL_ADDRESS, - kyberNetworkProxy: NULL_ADDRESS, - oasis: NULL_ADDRESS, - sushiswapRouter: NULL_ADDRESS, - uniswapV2Router: NULL_ADDRESS, - uniswapExchangeFactory: NULL_ADDRESS, - mStable: NULL_ADDRESS, - weth: NULL_ADDRESS, - shellBridge: NULL_ADDRESS, - creamBridge: NULL_ADDRESS, - dodoBridge: NULL_ADDRESS, - dodoHelper: NULL_ADDRESS, - snowSwapBridge: NULL_ADDRESS, - cryptoComBridge: NULL_ADDRESS, - bancorBridge: NULL_ADDRESS, - cofixBridge: NULL_ADDRESS, - }, + NULL_ADDRESS, ); transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync( artifacts.FillQuoteTransformer, env.provider, env.txDefaults, artifacts, - exchange.address, bridgeAdapter.address, + exchange.address, ); host = await TestFillQuoteTransformerHostContract.deployFrom0xArtifactAsync( artifacts.TestFillQuoteTransformerHost, @@ -115,146 +104,240 @@ blockchainTests.resets('FillQuoteTransformer', env => { ), ), ); - singleProtocolFee = (await exchange.protocolFeeMultiplier().callAsync()).times(GAS_PRICE); + singleProtocolFee = (await exchange.getProtocolFeeMultiplier().callAsync()).times(GAS_PRICE); }); - type FilledOrder = Order & { filledTakerAssetAmount: BigNumber }; - - function createOrder(fields: Partial = {}): FilledOrder { - return { - chainId: 1, - exchangeAddress: exchange.address, - expirationTimeSeconds: ZERO_AMOUNT, - salt: ZERO_AMOUNT, - senderAddress: NULL_ADDRESS, - takerAddress: NULL_ADDRESS, - makerAddress: maker, - feeRecipientAddress: feeRecipient, - makerAssetAmount: getRandomInteger('0.1e18', '1e18'), - takerAssetAmount: getRandomInteger('0.1e18', '1e18'), - makerFee: ZERO_AMOUNT, - takerFee: getRandomInteger('0.001e18', '0.1e18'), - makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), - takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), - makerFeeAssetData: NULL_BYTES, - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), - filledTakerAssetAmount: ZERO_AMOUNT, + function createLimitOrder(fields: Partial = {}): LimitOrder { + return getRandomLimitOrder({ + maker, + feeRecipient, + makerToken: makerToken.address, + takerToken: takerToken.address, + makerAmount: getRandomInteger('0.1e18', '1e18'), + takerAmount: getRandomInteger('0.1e18', '1e18'), + takerTokenFeeAmount: getRandomInteger('0.1e18', '1e18'), ...fields, + }); + } + + function createRfqOrder(fields: Partial = {}): RfqOrder { + return getRandomRfqOrder({ + maker, + makerToken: makerToken.address, + takerToken: takerToken.address, + makerAmount: getRandomInteger('0.1e18', '1e18'), + takerAmount: getRandomInteger('0.1e18', '1e18'), + ...fields, + }); + } + + function createBridgeOrder(fillRatio: Numberish = 1.0): BridgeOrder { + const makerTokenAmount = getRandomInteger('0.1e18', '1e18'); + return { + makerTokenAmount, + source: TEST_BRIDGE_SOURCE, + takerTokenAmount: getRandomInteger('0.1e18', '1e18'), + bridgeData: encodeBridgeData(makerTokenAmount.times(fillRatio).integerValue()), }; } - function createBridgeOrder(fields: Partial = {}, fillRatio: Numberish = 1.0): FilledOrder { - const order = createOrder(fields); - const bridgeData = encodeBridgeBehavior(order.makerAssetAmount, fillRatio); + function createOrderSignature(preFilledTakerAmount: Numberish = 0): Signature { return { - ...order, - makerAddress: bridge.address, - makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(makerToken.address, bridge.address, bridgeData), - makerFeeAssetData: NULL_BYTES, - takerFeeAssetData: NULL_BYTES, - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, + // The r field of the signature is the pre-filled amount. + r: hexUtils.leftPad(preFilledTakerAmount), + s: NULL_BYTES, + v: 0, + signatureType: 0, }; } + function orderSignatureToPreFilledTakerAmount(signature: Signature): BigNumber { + return new BigNumber(signature.r); + } + + function encodeBridgeData(boughtAmount: BigNumber): string { + // abi.encode(bridgeAddress, bridgeData) + return hexUtils.concat(hexUtils.leftPad(bridge.address), hexUtils.leftPad(32), hexUtils.leftPad(boughtAmount)); + } + interface QuoteFillResults { - makerAssetBought: BigNumber; - takerAssetSpent: BigNumber; + makerTokensBought: BigNumber; + takerTokensSpent: BigNumber; protocolFeePaid: BigNumber; } - const ZERO_QUOTE_FILL_RESULTS = { - makerAssetBought: ZERO_AMOUNT, - takerAssetSpent: ZERO_AMOUNT, - protocolFeePaid: ZERO_AMOUNT, - }; - - function getExpectedSellQuoteFillResults( - orders: FilledOrder[], - takerAssetFillAmount: BigNumber = constants.MAX_UINT256, - ): QuoteFillResults { - const qfr = { ...ZERO_QUOTE_FILL_RESULTS }; - for (const order of orders) { - if (qfr.takerAssetSpent.gte(takerAssetFillAmount)) { - break; - } - const singleFillAmount = BigNumber.min( - takerAssetFillAmount.minus(qfr.takerAssetSpent), - order.takerAssetAmount.minus(order.filledTakerAssetAmount), - ); - const fillRatio = singleFillAmount.div(order.takerAssetAmount); - qfr.takerAssetSpent = qfr.takerAssetSpent.plus(singleFillAmount); - qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); - qfr.makerAssetBought = qfr.makerAssetBought.plus( - fillRatio.times(order.makerAssetAmount).integerValue(BigNumber.ROUND_DOWN), - ); - const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_DOWN); - if (order.takerAssetData === order.takerFeeAssetData) { - // Taker fee is in taker asset. - qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee); - } else if (order.makerAssetData === order.takerFeeAssetData) { - // Taker fee is in maker asset. - qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee); - } - } - return qfr; + interface SimulationState { + takerTokenBalance: BigNumber; + ethBalance: BigNumber; } - function getExpectedBuyQuoteFillResults( - orders: FilledOrder[], - makerAssetFillAmount: BigNumber = constants.MAX_UINT256, + function getExpectedQuoteFillResults( + data: FillQuoteTransformerData, + state: SimulationState = createSimulationState(), ): QuoteFillResults { - const qfr = { ...ZERO_QUOTE_FILL_RESULTS }; - for (const order of orders) { - if (qfr.makerAssetBought.gte(makerAssetFillAmount)) { - break; - } - const filledMakerAssetAmount = order.filledTakerAssetAmount - .times(order.makerAssetAmount.div(order.takerAssetAmount)) - .integerValue(BigNumber.ROUND_DOWN); - const singleFillAmount = BigNumber.min( - makerAssetFillAmount.minus(qfr.makerAssetBought), - order.makerAssetAmount.minus(filledMakerAssetAmount), - ); - const fillRatio = singleFillAmount.div(order.makerAssetAmount); - qfr.takerAssetSpent = qfr.takerAssetSpent.plus( - fillRatio.times(order.takerAssetAmount).integerValue(BigNumber.ROUND_UP), - ); - qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); - qfr.makerAssetBought = qfr.makerAssetBought.plus(singleFillAmount); - const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_UP); - if (order.takerAssetData === order.takerFeeAssetData) { - // Taker fee is in taker asset. - qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee); - } else if (order.makerAssetData === order.takerFeeAssetData) { - // Taker fee is in maker asset. - qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee); - } + const EMPTY_FILL_ORDER_RESULTS = { + takerTokenSoldAmount: ZERO_AMOUNT, + makerTokenBoughtAmount: ZERO_AMOUNT, + protocolFeePaid: ZERO_AMOUNT, + }; + type FillOrderResults = typeof EMPTY_FILL_ORDER_RESULTS; + + let takerTokenBalanceRemaining = state.takerTokenBalance; + if (data.side === Side.Sell && !data.fillAmount.eq(MAX_UINT256)) { + takerTokenBalanceRemaining = data.fillAmount; } - return qfr; + let ethBalanceRemaining = state.ethBalance; + let soldAmount = ZERO_AMOUNT; + let boughtAmount = ZERO_AMOUNT; + const fillAmount = normalizeFillAmount(data.fillAmount, state.takerTokenBalance); + const orderIndices = [0, 0, 0]; + + function computeTakerTokenFillAmount( + orderTakerTokenAmount: BigNumber, + orderMakerTokenAmount: BigNumber, + orderTakerTokenFeeAmount: BigNumber = ZERO_AMOUNT, + ): BigNumber { + let takerTokenFillAmount = ZERO_AMOUNT; + if (data.side === Side.Sell) { + takerTokenFillAmount = fillAmount.minus(soldAmount); + if (orderTakerTokenFeeAmount.gt(0)) { + takerTokenFillAmount = takerTokenFillAmount + .times(orderTakerTokenAmount) + .div(orderTakerTokenAmount.plus(orderTakerTokenFeeAmount)) + .integerValue(BigNumber.ROUND_UP); + } + } else { + // Buy + takerTokenFillAmount = fillAmount + .minus(boughtAmount) + .times(orderTakerTokenAmount) + .div(orderMakerTokenAmount) + .integerValue(BigNumber.ROUND_UP); + } + return BigNumber.min(takerTokenFillAmount, orderTakerTokenAmount, takerTokenBalanceRemaining); + } + + function fillBridgeOrder(order: BridgeOrder): FillOrderResults { + const bridgeBoughtAmount = decodeBridgeData(order.bridgeData).boughtAmount; + if (bridgeBoughtAmount.eq(REVERT_AMOUNT)) { + return EMPTY_FILL_ORDER_RESULTS; + } + return { + ...EMPTY_FILL_ORDER_RESULTS, + takerTokenSoldAmount: computeTakerTokenFillAmount(order.takerTokenAmount, order.makerTokenAmount), + makerTokenBoughtAmount: bridgeBoughtAmount, + }; + } + + function fillLimitOrder(oi: FillQuoteTransformerLimitOrderInfo): FillOrderResults { + const preFilledTakerAmount = orderSignatureToPreFilledTakerAmount(oi.signature); + if (preFilledTakerAmount.gte(oi.order.takerAmount) || preFilledTakerAmount.eq(REVERT_AMOUNT)) { + return EMPTY_FILL_ORDER_RESULTS; + } + if (ethBalanceRemaining.lt(singleProtocolFee)) { + return EMPTY_FILL_ORDER_RESULTS; + } + const takerTokenFillAmount = BigNumber.min( + computeTakerTokenFillAmount(oi.order.takerAmount, oi.order.makerAmount, oi.order.takerTokenFeeAmount), + oi.order.takerAmount.minus(preFilledTakerAmount), + oi.maxTakerTokenFillAmount, + ); + const fillRatio = takerTokenFillAmount.div(oi.order.takerAmount); + return { + ...EMPTY_FILL_ORDER_RESULTS, + takerTokenSoldAmount: takerTokenFillAmount.plus( + fillRatio.times(oi.order.takerTokenFeeAmount).integerValue(BigNumber.ROUND_DOWN), + ), + makerTokenBoughtAmount: fillRatio.times(oi.order.makerAmount).integerValue(BigNumber.ROUND_DOWN), + protocolFeePaid: singleProtocolFee, + }; + } + + function fillRfqOrder(oi: FillQuoteTransformerRfqOrderInfo): FillOrderResults { + const preFilledTakerAmount = orderSignatureToPreFilledTakerAmount(oi.signature); + if (preFilledTakerAmount.gte(oi.order.takerAmount) || preFilledTakerAmount.eq(REVERT_AMOUNT)) { + return EMPTY_FILL_ORDER_RESULTS; + } + const takerTokenFillAmount = BigNumber.min( + computeTakerTokenFillAmount(oi.order.takerAmount, oi.order.makerAmount), + oi.order.takerAmount.minus(preFilledTakerAmount), + oi.maxTakerTokenFillAmount, + ); + const fillRatio = takerTokenFillAmount.div(oi.order.takerAmount); + return { + ...EMPTY_FILL_ORDER_RESULTS, + takerTokenSoldAmount: takerTokenFillAmount, + makerTokenBoughtAmount: fillRatio.times(oi.order.makerAmount).integerValue(BigNumber.ROUND_DOWN), + }; + } + + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < data.fillSequence.length; ++i) { + const orderType = data.fillSequence[i]; + if (data.side === Side.Sell) { + if (soldAmount.gte(fillAmount)) { + break; + } + } else { + if (boughtAmount.gte(fillAmount)) { + break; + } + } + let results = EMPTY_FILL_ORDER_RESULTS; + switch (orderType) { + case OrderType.Bridge: + { + results = fillBridgeOrder(data.bridgeOrders[orderIndices[orderType]]); + } + break; + case OrderType.Limit: + { + results = fillLimitOrder(data.limitOrders[orderIndices[orderType]]); + } + break; + case OrderType.Rfq: + { + results = fillRfqOrder(data.rfqOrders[orderIndices[orderType]]); + } + break; + default: + throw new Error('Unknown order type'); + } + soldAmount = soldAmount.plus(results.takerTokenSoldAmount); + boughtAmount = boughtAmount.plus(results.makerTokenBoughtAmount); + ethBalanceRemaining = ethBalanceRemaining.minus(results.protocolFeePaid); + takerTokenBalanceRemaining = takerTokenBalanceRemaining.minus(results.takerTokenSoldAmount); + orderIndices[orderType]++; + } + + return { + takerTokensSpent: soldAmount, + makerTokensBought: boughtAmount, + protocolFeePaid: state.ethBalance.minus(ethBalanceRemaining), + }; } interface Balances { - makerAssetBalance: BigNumber; - takerAssetBalance: BigNumber; + makerTokenBalance: BigNumber; + takerTokensBalance: BigNumber; takerFeeBalance: BigNumber; - protocolFeeBalance: BigNumber; + ethBalance: BigNumber; } const ZERO_BALANCES = { - makerAssetBalance: ZERO_AMOUNT, - takerAssetBalance: ZERO_AMOUNT, + makerTokenBalance: ZERO_AMOUNT, + takerTokensBalance: ZERO_AMOUNT, takerFeeBalance: ZERO_AMOUNT, - protocolFeeBalance: ZERO_AMOUNT, + ethBalance: ZERO_AMOUNT, }; async function getBalancesAsync(owner: string): Promise { const balances = { ...ZERO_BALANCES }; [ - balances.makerAssetBalance, - balances.takerAssetBalance, + balances.makerTokenBalance, + balances.takerTokensBalance, balances.takerFeeBalance, - balances.protocolFeeBalance, + balances.ethBalance, ] = await Promise.all([ makerToken.balanceOf(owner).callAsync(), takerToken.balanceOf(owner).callAsync(), @@ -265,875 +348,1013 @@ blockchainTests.resets('FillQuoteTransformer', env => { } function assertBalances(actual: Balances, expected: Balances): void { - assertIntegerRoughlyEquals(actual.makerAssetBalance, expected.makerAssetBalance, 10, 'makerAssetBalance'); - assertIntegerRoughlyEquals(actual.takerAssetBalance, expected.takerAssetBalance, 10, 'takerAssetBalance'); + assertIntegerRoughlyEquals(actual.makerTokenBalance, expected.makerTokenBalance, 10, 'makerTokenBalance'); + assertIntegerRoughlyEquals(actual.takerTokensBalance, expected.takerTokensBalance, 10, 'takerTokensBalance'); assertIntegerRoughlyEquals(actual.takerFeeBalance, expected.takerFeeBalance, 10, 'takerFeeBalance'); - assertIntegerRoughlyEquals(actual.protocolFeeBalance, expected.protocolFeeBalance, 10, 'protocolFeeBalance'); + assertIntegerRoughlyEquals(actual.ethBalance, expected.ethBalance, 10, 'ethBalance'); } - function encodeTransformData(fields: Partial = {}): string { - return encodeFillQuoteTransformerData({ - side: FillQuoteTransformerSide.Sell, + async function assertCurrentBalancesAsync(owner: string, expected: Balances): Promise { + assertBalances(await getBalancesAsync(owner), expected); + } + + function encodeFractionalFillAmount(frac: number): BigNumber { + return HIGH_BIT.plus(new BigNumber(frac).times('1e18').integerValue()); + } + + function normalizeFillAmount(raw: BigNumber, balance: BigNumber): BigNumber { + if (raw.gte(HIGH_BIT)) { + return raw + .minus(HIGH_BIT) + .div('1e18') + .times(balance) + .integerValue(BigNumber.ROUND_DOWN); + } + return raw; + } + + interface BridgeData { + bridge: string; + boughtAmount: BigNumber; + } + + function decodeBridgeData(encoded: string): BridgeData { + return { + bridge: hexUtils.slice(encoded, 0, 32), + boughtAmount: new BigNumber(hexUtils.slice(encoded, 64)), + }; + } + + function createTransformData(fields: Partial = {}): FillQuoteTransformerData { + return { + side: Side.Sell, sellToken: takerToken.address, buyToken: makerToken.address, - orders: [], - signatures: [], - maxOrderFillAmounts: [], + bridgeOrders: [], + limitOrders: [], + rfqOrders: [], + fillSequence: [], fillAmount: MAX_UINT256, refundReceiver: NULL_ADDRESS, - rfqtTakerAddress: NULL_ADDRESS, ...fields, + }; + } + + function createSimulationState(fields: Partial = {}): SimulationState { + return { + ethBalance: ZERO_AMOUNT, + takerTokenBalance: ZERO_AMOUNT, + ...fields, + }; + } + + interface ExecuteTransformParams { + takerTokenBalance: BigNumber; + ethBalance: BigNumber; + sender: string; + taker: string; + data: FillQuoteTransformerData; + } + + async function executeTransformAsync(params: Partial = {}): Promise { + const data = params.data || createTransformData(params.data); + const _params = { + takerTokenBalance: data.fillAmount, + sender, + taker, + data, + ...params, + }; + return host + .executeTransform( + transformer.address, + takerToken.address, + _params.takerTokenBalance, + _params.sender, + _params.taker, + encodeFillQuoteTransformerData(_params.data), + ) + .awaitTransactionSuccessAsync({ value: _params.ethBalance }); + } + + async function assertFinalBalancesAsync(qfr: QuoteFillResults): Promise { + await assertCurrentBalancesAsync(host.address, { + ...ZERO_BALANCES, + makerTokenBalance: qfr.makerTokensBought, }); + await assertCurrentBalancesAsync(exchange.address, { ...ZERO_BALANCES, ethBalance: qfr.protocolFeePaid }); } - function encodeExchangeBehavior( - filledTakerAssetAmount: Numberish = 0, - makerAssetMintRatio: Numberish = 1.0, - ): string { - return hexUtils.slice( - exchange - .encodeBehaviorData({ - filledTakerAssetAmount: new BigNumber(filledTakerAssetAmount), - makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(), - }) - .getABIEncodedTransactionData(), - 4, - ); - } - - function encodeBridgeBehavior(amount: BigNumber, makerAssetMintRatio: Numberish = 1.0): string { - return hexUtils.slice( - bridge - .encodeBehaviorData({ - makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(), - amount, - }) - .getABIEncodedTransactionData(), - 4, - ); - } - - const ERC20_ASSET_PROXY_ID = '0xf47261b0'; - describe('sell quotes', () => { - it('can fully sell to a single order quote', async () => { - const orders = _.times(1, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + it('can fully sell to a single bridge order with -1 fillAmount', async () => { + const bridgeOrders = [createBridgeOrder()]; + const data = createTransformData({ + bridgeOrders, + fillAmount: BigNumber.sum(...bridgeOrders.map(o => o.takerTokenAmount)), + fillSequence: bridgeOrders.map(() => OrderType.Bridge), }); + const qfr = getExpectedQuoteFillResults(data); + await executeTransformAsync({ + takerTokenBalance: data.fillAmount, + data: { ...data, fillAmount: MAX_UINT256 }, + }); + return assertFinalBalancesAsync(qfr); }); - it('can fully sell to multi order quote', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + it('can partially sell to a single bridge order with a fractional fillAmount', async () => { + const bridgeOrders = [createBridgeOrder()]; + const totalTakerBalance = BigNumber.sum(...bridgeOrders.map(o => o.takerTokenAmount)); + const data = createTransformData({ + bridgeOrders, + fillAmount: encodeFractionalFillAmount(0.5), + fillSequence: bridgeOrders.map(() => OrderType.Bridge), }); - }); - - it('can partially sell to single order quote', async () => { - const orders = _.times(1, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults( - orders, - getExpectedSellQuoteFillResults(orders).takerAssetSpent.dividedToIntegerBy(2), + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ takerTokenBalance: totalTakerBalance }), ); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { + await executeTransformAsync({ + takerTokenBalance: totalTakerBalance, + data, + }); + await assertCurrentBalancesAsync(host.address, { ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + takerTokensBalance: totalTakerBalance.minus(qfr.takerTokensSpent), + makerTokenBalance: qfr.makerTokensBought, }); }); - it('can partially sell to multi order quote and refund unused protocol fees', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders.slice(0, 2)); - const maxProtocolFees = singleProtocolFee.times(orders.length); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: maxProtocolFees }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - protocolFeeBalance: singleProtocolFee, + it('fails if incomplete sell', async () => { + const bridgeOrders = [createBridgeOrder()]; + const data = createTransformData({ + bridgeOrders, + fillAmount: BigNumber.sum(...bridgeOrders.map(o => o.takerTokenAmount)), + fillSequence: bridgeOrders.map(() => OrderType.Bridge), }); - }); - - it('can sell to multi order quote with a failing order', async () => { - const orders = _.times(3, () => createOrder()); - // First order will fail. - const validOrders = orders.slice(1); - const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())]; - const qfr = getExpectedSellQuoteFillResults(validOrders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + const tx = executeTransformAsync({ + takerTokenBalance: data.fillAmount, + data: { ...data, fillAmount: data.fillAmount.plus(1) }, }); - }); - - it('succeeds if an order transfers too few maker tokens', async () => { - const mintScale = 0.5; - const orders = _.times(3, () => createOrder()); - // First order mints less than expected. - const signatures = [ - encodeExchangeBehavior(0, mintScale), - ...orders.slice(1).map(() => encodeExchangeBehavior()), - ]; - const qfr = getExpectedSellQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought - .minus(orders[0].makerAssetAmount.times(1 - mintScale)) - .integerValue(BigNumber.ROUND_DOWN), - }); - }); - - it('can fail if an order is partially filled', async () => { - const orders = _.times(3, () => createOrder()); - // First order is partially filled. - const filledOrder = { - ...orders[0], - filledTakerAssetAmount: orders[0].takerAssetAmount.dividedToIntegerBy(2), - }; - // First order is partially filled. - const signatures = [ - encodeExchangeBehavior(filledOrder.filledTakerAssetAmount), - ...orders.slice(1).map(() => encodeExchangeBehavior()), - ]; - const qfr = getExpectedSellQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); return expect(tx).to.revertWith( new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( - takerToken.address, - getExpectedSellQuoteFillResults([filledOrder, ...orders.slice(1)]).takerAssetSpent, - qfr.takerAssetSpent, + data.sellToken, + data.fillAmount, + data.fillAmount.plus(1), ), ); }); - it('fails if not enough protocol fee provided', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( - takerToken.address, - getExpectedSellQuoteFillResults([...orders.slice(0, 2)]).takerAssetSpent, - qfr.takerAssetSpent, - ), - ); - }); - - it('can sell less than the taker token balance', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const takerTokenBalance = qfr.takerAssetSpent.times(1.01).integerValue(); - await host - .executeTransform( - transformer.address, - takerToken.address, - takerTokenBalance, - sender, - taker, - encodeTransformData({ - orders, - signatures, - fillAmount: qfr.takerAssetSpent, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - takerAssetBalance: qfr.takerAssetSpent.times(0.01).integerValue(), + it('can fully sell to a single bridge order', async () => { + const bridgeOrders = [createBridgeOrder()]; + const data = createTransformData({ + bridgeOrders, + fillAmount: BigNumber.sum(...bridgeOrders.map(o => o.takerTokenAmount)), + fillSequence: bridgeOrders.map(() => OrderType.Bridge), }); + const qfr = getExpectedQuoteFillResults(data); + await executeTransformAsync({ + takerTokenBalance: data.fillAmount, + data, + }); + return assertFinalBalancesAsync(qfr); }); - it('fails to sell more than the taker token balance', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const takerTokenBalance = qfr.takerAssetSpent.times(0.99).integerValue(); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - takerTokenBalance, - sender, - taker, - encodeTransformData({ - orders, - signatures, - fillAmount: qfr.takerAssetSpent, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( - takerToken.address, - getExpectedSellQuoteFillResults(orders.slice(0, 2)).takerAssetSpent, - qfr.takerAssetSpent, - ), - ); - }); - - it('can fully sell to a single order with maker asset taker fees', async () => { - const orders = _.times(1, () => - createOrder({ - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + it('can fully sell to a single limit order', async () => { + const limitOrders = [createLimitOrder()]; + const data = createTransformData({ + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount))), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), }), ); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, }); + return assertFinalBalancesAsync(qfr); }); - it('fails if an order has a non-standard taker fee asset', async () => { - const BAD_ASSET_DATA = hexUtils.random(36); - const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA), + it('can partial sell to a single limit order', async () => { + const limitOrders = [createLimitOrder()]; + const data = createTransformData({ + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum( + ...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount)), + ).dividedToIntegerBy(2), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + }), ); - }); - - it('fails if an order has a fee asset that is neither maker or taker asset', async () => { - const badToken = randomAddress(); - const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); - const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken)); - }); - - it('respects `maxOrderFillAmounts`', async () => { - const orders = _.times(2, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders.slice(1)); - const protocolFee = singleProtocolFee.times(2); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - // Skip the first order. - maxOrderFillAmounts: [ZERO_AMOUNT], - }), - ) - .awaitTransactionSuccessAsync({ value: protocolFee }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, }); + return assertFinalBalancesAsync(qfr); }); - it('can refund unspent protocol fee to the `refundReceiver`', async () => { - const orders = _.times(2, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const protocolFee = qfr.protocolFeePaid.plus(1); - const refundReceiver = randomAddress(); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - refundReceiver, - }), - ) - .awaitTransactionSuccessAsync({ value: protocolFee }); - const receiverBalancer = await env.web3Wrapper.getBalanceInWeiAsync(refundReceiver); - expect(receiverBalancer).to.bignumber.eq(1); + it('can fully sell to a single limit order without fees', async () => { + const limitOrders = [createLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })]; + const data = createTransformData({ + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount))), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + return assertFinalBalancesAsync(qfr); }); - it('can refund unspent protocol fee to the taker', async () => { - const orders = _.times(2, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const protocolFee = qfr.protocolFeePaid.plus(1); - const refundReceiver = randomAddress(); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - refundReceiver, // taker = refundReceiver - encodeTransformData({ - orders, - signatures, - // address(1) indicates taker - refundReceiver: hexUtils.leftPad(1, 20), - }), - ) - .awaitTransactionSuccessAsync({ value: protocolFee }); - const receiverBalancer = await env.web3Wrapper.getBalanceInWeiAsync(refundReceiver); - expect(receiverBalancer).to.bignumber.eq(1); + it('can partial sell to a single limit order without fees', async () => { + const limitOrders = [createLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })]; + const data = createTransformData({ + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum( + ...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount)), + ).dividedToIntegerBy(2), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + return assertFinalBalancesAsync(qfr); }); - it('can refund unspent protocol fee to the sender', async () => { - const orders = _.times(2, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); - const protocolFee = qfr.protocolFeePaid.plus(1); - const refundReceiver = randomAddress(); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - refundReceiver, // sender = refundReceiver - taker, - encodeTransformData({ - orders, - signatures, - // address(2) indicates sender - refundReceiver: hexUtils.leftPad(2, 20), - }), + it('can fully sell to a single RFQ order', async () => { + const rfqOrders = [createRfqOrder()]; + const data = createTransformData({ + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...rfqOrders.map(o => o.takerAmount)), + fillSequence: rfqOrders.map(() => OrderType.Rfq), + }); + const qfr = getExpectedQuoteFillResults(data, createSimulationState()); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + }); + return assertFinalBalancesAsync(qfr); + }); + + it('can partially sell to a single RFQ order', async () => { + const rfqOrders = [createRfqOrder()]; + const data = createTransformData({ + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...rfqOrders.map(o => o.takerAmount)).dividedToIntegerBy(2), + fillSequence: rfqOrders.map(() => OrderType.Rfq), + }); + const qfr = getExpectedQuoteFillResults(data, createSimulationState()); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + }); + return assertFinalBalancesAsync(qfr); + }); + + it('can fully sell to one of each order type', async () => { + const rfqOrders = [createRfqOrder()]; + const limitOrders = [createLimitOrder()]; + const bridgeOrders = [createBridgeOrder()]; + const data = createTransformData({ + bridgeOrders, + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum( + ...rfqOrders.map(o => o.takerAmount), + ...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount)), + ...bridgeOrders.map(o => o.takerTokenAmount), + ), + fillSequence: _.shuffle([ + ...bridgeOrders.map(() => OrderType.Bridge), + ...rfqOrders.map(() => OrderType.Rfq), + ...limitOrders.map(() => OrderType.Limit), + ]), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); + }); + + it('can partially sell to one of each order type', async () => { + const rfqOrders = [createRfqOrder()]; + const limitOrders = [createLimitOrder()]; + const bridgeOrders = [createBridgeOrder()]; + const data = createTransformData({ + bridgeOrders, + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum( + ...rfqOrders.map(o => o.takerAmount), + ...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount)), + ...bridgeOrders.map(o => o.takerTokenAmount), + ).dividedToIntegerBy(2), + fillSequence: _.shuffle([ + ...bridgeOrders.map(() => OrderType.Bridge), + ...rfqOrders.map(() => OrderType.Rfq), + ...limitOrders.map(() => OrderType.Limit), + ]), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); + }); + + it('can fully sell to multiple of each order type', async () => { + const rfqOrders = _.times(2, () => createRfqOrder()); + const limitOrders = _.times(3, () => createLimitOrder()); + const bridgeOrders = _.times(4, () => createBridgeOrder()); + const data = createTransformData({ + bridgeOrders, + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum( + ...rfqOrders.map(o => o.takerAmount), + ...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount)), + ...bridgeOrders.map(o => o.takerTokenAmount), + ), + fillSequence: _.shuffle([ + ...bridgeOrders.map(() => OrderType.Bridge), + ...rfqOrders.map(() => OrderType.Rfq), + ...limitOrders.map(() => OrderType.Limit), + ]), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); + }); + + it('can recover from a failed order', async () => { + const rfqOrder = createRfqOrder(); + const limitOrder = createLimitOrder(); + const bridgeOrder = createBridgeOrder(); + const fillSequence = _.shuffle([OrderType.Bridge, OrderType.Rfq, OrderType.Limit]); + // Fail the first order in the sequence. + const failedOrderType = fillSequence[0]; + const data = createTransformData({ + fillSequence, + bridgeOrders: [ + { + ...bridgeOrder, + bridgeData: + failedOrderType === OrderType.Bridge + ? encodeBridgeData(REVERT_AMOUNT) + : bridgeOrder.bridgeData, + }, + ], + rfqOrders: [ + { + order: rfqOrder, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature( + failedOrderType === OrderType.Rfq ? REVERT_AMOUNT : ZERO_AMOUNT, + ), + }, + ], + limitOrders: [ + { + order: limitOrder, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature( + failedOrderType === OrderType.Limit ? REVERT_AMOUNT : ZERO_AMOUNT, + ), + }, + ], + // Only require the last two orders to be filled. + fillAmount: BigNumber.sum( + rfqOrder.takerAmount, + limitOrder.takerAmount.plus(limitOrder.takerTokenFeeAmount), + bridgeOrder.takerTokenAmount, ) - .awaitTransactionSuccessAsync({ value: protocolFee }); - const receiverBalancer = await env.web3Wrapper.getBalanceInWeiAsync(refundReceiver); - expect(receiverBalancer).to.bignumber.eq(1); + .minus(failedOrderType === OrderType.Bridge ? bridgeOrder.takerTokenAmount : 0) + .minus(failedOrderType === OrderType.Rfq ? rfqOrder.takerAmount : 0) + .minus( + failedOrderType === OrderType.Limit + ? limitOrder.takerAmount.plus(limitOrder.takerTokenFeeAmount) + : 0, + ), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); + }); + + it('can recover from a slipped order', async () => { + const rfqOrder = createRfqOrder(); + const limitOrder = createLimitOrder(); + const bridgeOrder = createBridgeOrder(); + const fillSequence = _.shuffle([OrderType.Bridge, OrderType.Rfq, OrderType.Limit]); + // Slip the first order in the sequence. + const slippedOrderType = fillSequence[0]; + const data = createTransformData({ + fillSequence, + bridgeOrders: [ + { + ...bridgeOrder, + // If slipped, produce half the tokens. + bridgeData: + slippedOrderType === OrderType.Bridge + ? encodeBridgeData(bridgeOrder.makerTokenAmount.dividedToIntegerBy(2)) + : bridgeOrder.bridgeData, + }, + ], + rfqOrders: [ + { + order: rfqOrder, + maxTakerTokenFillAmount: MAX_UINT256, + // If slipped, set half the order to filled. + signature: createOrderSignature( + slippedOrderType === OrderType.Rfq + ? rfqOrder.takerAmount.div(2).integerValue(BigNumber.ROUND_DOWN) + : ZERO_AMOUNT, + ), + }, + ], + limitOrders: [ + { + order: limitOrder, + maxTakerTokenFillAmount: MAX_UINT256, + // If slipped, set half the order to filled. + signature: createOrderSignature( + slippedOrderType === OrderType.Limit + ? limitOrder.takerAmount.div(2).integerValue(BigNumber.ROUND_DOWN) + : ZERO_AMOUNT, + ), + }, + ], + // Only require half the first order to be filled. + fillAmount: BigNumber.sum( + rfqOrder.takerAmount, + limitOrder.takerAmount.plus(limitOrder.takerTokenFeeAmount), + bridgeOrder.takerTokenAmount, + ) + .minus( + slippedOrderType === OrderType.Bridge + ? bridgeOrder.takerTokenAmount.div(2).integerValue(BigNumber.ROUND_UP) + : 0, + ) + .minus( + slippedOrderType === OrderType.Rfq + ? rfqOrder.takerAmount.div(2).integerValue(BigNumber.ROUND_UP) + : 0, + ) + .minus( + slippedOrderType === OrderType.Limit + ? limitOrder.takerAmount + .plus(limitOrder.takerTokenFeeAmount) + .div(2) + .integerValue(BigNumber.ROUND_UP) + : 0, + ), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); + }); + + it('skips limit orders when not enough protocol fee balance', async () => { + const limitOrder = createLimitOrder(); + const bridgeOrder = { + source: TEST_BRIDGE_SOURCE, + makerTokenAmount: limitOrder.makerAmount, + takerTokenAmount: limitOrder.takerAmount, + bridgeData: encodeBridgeData(limitOrder.makerAmount), + }; + const fillSequence = [OrderType.Limit, OrderType.Bridge]; + const data = createTransformData({ + fillSequence, + bridgeOrders: [bridgeOrder], + limitOrders: [ + { + order: limitOrder, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + }, + ], + // Only require one order to be filled (they are both the same size). + fillAmount: bridgeOrder.takerTokenAmount, + }); + const qfr = getExpectedQuoteFillResults(data); + expect(qfr.takerTokensSpent).to.bignumber.eq(bridgeOrder.takerTokenAmount); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + }); + await assertFinalBalancesAsync(qfr); }); }); describe('buy quotes', () => { - it('can fully buy from a single order quote', async () => { - const orders = _.times(1, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + it('fails if incomplete buy', async () => { + const bridgeOrders = [createBridgeOrder()]; + const data = createTransformData({ + bridgeOrders, + side: Side.Buy, + fillAmount: BigNumber.sum(...bridgeOrders.map(o => o.makerTokenAmount)), + fillSequence: bridgeOrders.map(() => OrderType.Bridge), }); - }); - - it('can fully buy from a multi order quote', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + const tx = executeTransformAsync({ + takerTokenBalance: BigNumber.sum(...bridgeOrders.map(o => o.takerTokenAmount)), + data: { ...data, fillAmount: data.fillAmount.plus(1) }, }); - }); - - it('can partially buy from a single order quote', async () => { - const orders = _.times(1, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults( - orders, - getExpectedBuyQuoteFillResults(orders).makerAssetBought.dividedToIntegerBy(2), - ); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - }); - }); - - it('can partially buy from multi order quote and refund unused protocol fees', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders.slice(0, 2)); - const maxProtocolFees = singleProtocolFee.times(orders.length); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: maxProtocolFees }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - protocolFeeBalance: singleProtocolFee, - }); - }); - - it('can buy from multi order quote with a failing order', async () => { - const orders = _.times(3, () => createOrder()); - // First order will fail. - const validOrders = orders.slice(1); - const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())]; - const qfr = getExpectedBuyQuoteFillResults(validOrders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - }); - }); - - it('fails to buy more than available in orders', async () => { - const orders = _.times(3, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought.plus(1), - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); return expect(tx).to.revertWith( new ZeroExRevertErrors.TransformERC20.IncompleteFillBuyQuoteError( - makerToken.address, - qfr.makerAssetBought, - qfr.makerAssetBought.plus(1), + data.buyToken, + data.fillAmount, + data.fillAmount.plus(1), ), ); }); - it('can fully buy from a single order with maker asset taker fees', async () => { - const orders = _.times(1, () => - createOrder({ - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + it('can fully buy to a single bridge order', async () => { + const bridgeOrders = [createBridgeOrder()]; + const totalTakerTokens = BigNumber.sum(...bridgeOrders.map(o => o.takerTokenAmount)); + const data = createTransformData({ + bridgeOrders, + side: Side.Buy, + fillAmount: BigNumber.sum(...bridgeOrders.map(o => o.makerTokenAmount)), + fillSequence: bridgeOrders.map(() => OrderType.Bridge), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ takerTokenBalance: totalTakerTokens }), + ); + await executeTransformAsync({ + takerTokenBalance: totalTakerTokens, + data, + }); + return assertFinalBalancesAsync(qfr); + }); + + it('can fully buy to a single limit order', async () => { + const limitOrders = [createLimitOrder()]; + const totalTakerTokens = BigNumber.sum(...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount))); + const data = createTransformData({ + side: Side.Buy, + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...limitOrders.map(o => o.makerAmount)), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + takerTokenBalance: totalTakerTokens, }), ); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + await executeTransformAsync({ + data, + takerTokenBalance: totalTakerTokens, + ethBalance: qfr.protocolFeePaid, }); + return assertFinalBalancesAsync(qfr); }); - it('fails if an order has a non-standard taker fee asset', async () => { - const BAD_ASSET_DATA = hexUtils.random(36); - const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA), + it('can partial buy to a single limit order', async () => { + const limitOrders = [createLimitOrder()]; + const totalTakerTokens = BigNumber.sum(...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount))); + const data = createTransformData({ + side: Side.Buy, + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...limitOrders.map(o => o.makerAmount)).dividedToIntegerBy(2), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + takerTokenBalance: totalTakerTokens, + }), ); - }); - - it('fails if an order has a fee asset that is neither maker or taker asset', async () => { - const badToken = randomAddress(); - const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); - const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders); - const tx = host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - }), - ) - .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); - return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken)); - }); - - it('respects `maxOrderFillAmounts`', async () => { - const orders = _.times(2, () => createOrder()); - const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedBuyQuoteFillResults(orders.slice(1)); - const protocolFee = singleProtocolFee.times(2); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - side: FillQuoteTransformerSide.Buy, - fillAmount: qfr.makerAssetBought, - // Skip the first order. - maxOrderFillAmounts: [ZERO_AMOUNT], - }), - ) - .awaitTransactionSuccessAsync({ value: protocolFee }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, }); + return assertFinalBalancesAsync(qfr); }); - }); - describe('bridge orders fall through', () => { - it('can fully sell to a single bridge order quote', async () => { - const orders = _.times(1, () => createBridgeOrder()); - const signatures = orders.map(() => NULL_BYTES); - const qfr = getExpectedSellQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: ZERO_AMOUNT }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + it('can fully buy to a single limit order without fees', async () => { + const limitOrders = [createLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })]; + const totalTakerTokens = BigNumber.sum(...limitOrders.map(o => o.takerAmount)); + const data = createTransformData({ + side: Side.Buy, + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...limitOrders.map(o => o.makerAmount)), + fillSequence: limitOrders.map(() => OrderType.Limit), }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + takerTokenBalance: totalTakerTokens, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + return assertFinalBalancesAsync(qfr); }); - it('can sell to a mix of order quote', async () => { - const nativeOrders = [createOrder()]; + it('can partial buy to a single limit order without fees', async () => { + const limitOrders = [createLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })]; + const totalTakerTokens = BigNumber.sum(...limitOrders.map(o => o.takerAmount)); + const data = createTransformData({ + side: Side.Buy, + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...limitOrders.map(o => o.makerAmount)).dividedToIntegerBy(2), + fillSequence: limitOrders.map(() => OrderType.Limit), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + takerTokenBalance: totalTakerTokens, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + return assertFinalBalancesAsync(qfr); + }); + + it('can fully buy to a single RFQ order', async () => { + const rfqOrders = [createRfqOrder()]; + const totalTakerTokens = BigNumber.sum(...rfqOrders.map(o => o.takerAmount)); + const data = createTransformData({ + side: Side.Buy, + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...rfqOrders.map(o => o.makerAmount)), + fillSequence: rfqOrders.map(() => OrderType.Rfq), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ takerTokenBalance: totalTakerTokens }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + }); + return assertFinalBalancesAsync(qfr); + }); + + it('can partially buy to a single RFQ order', async () => { + const rfqOrders = [createRfqOrder()]; + const totalTakerTokens = BigNumber.sum(...rfqOrders.map(o => o.takerAmount)); + const data = createTransformData({ + side: Side.Buy, + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum(...rfqOrders.map(o => o.makerAmount)).dividedToIntegerBy(2), + fillSequence: rfqOrders.map(() => OrderType.Rfq), + }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ takerTokenBalance: totalTakerTokens }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + }); + return assertFinalBalancesAsync(qfr); + }); + + it('can fully buy to one of each order type', async () => { + const rfqOrders = [createRfqOrder()]; + const limitOrders = [createLimitOrder()]; const bridgeOrders = [createBridgeOrder()]; - const orders = [...nativeOrders, ...bridgeOrders]; - const signatures = [ - ...nativeOrders.map(() => encodeExchangeBehavior()), // Valid Signatures - ...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures - ]; - const qfr = getExpectedSellQuoteFillResults(orders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - .awaitTransactionSuccessAsync({ value: singleProtocolFee.times(nativeOrders.length) }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, + const totalTakerTokens = BigNumber.sum( + ...rfqOrders.map(o => o.takerAmount), + ...limitOrders.map(o => o.takerAmount.plus(o.takerTokenFeeAmount)), + ...bridgeOrders.map(o => o.takerTokenAmount), + ); + const data = createTransformData({ + side: Side.Buy, + bridgeOrders, + rfqOrders: rfqOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + limitOrders: limitOrders.map(o => ({ + order: o, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature(), + })), + fillAmount: BigNumber.sum( + ...rfqOrders.map(o => o.makerAmount), + ...limitOrders.map(o => o.makerAmount), + ...bridgeOrders.map(o => o.makerTokenAmount), + ), + fillSequence: _.shuffle([ + ...bridgeOrders.map(() => OrderType.Bridge), + ...rfqOrders.map(() => OrderType.Rfq), + ...limitOrders.map(() => OrderType.Limit), + ]), }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee.times(limitOrders.length), + takerTokenBalance: totalTakerTokens, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); }); - it('can attempt to sell to a mix of order quote handling reverts', async () => { - const nativeOrders = _.times(3, () => createOrder()); - const bridgeOrders = [createBridgeOrder()]; - const orders = [...nativeOrders, ...bridgeOrders]; - const signatures = [ - ...nativeOrders.map(() => NULL_BYTES), // Invalid Signatures - ...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures - ]; - const qfr = getExpectedSellQuoteFillResults(bridgeOrders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - // Single protocol fee as all Native orders will fail - .awaitTransactionSuccessAsync({ value: singleProtocolFee }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - protocolFeeBalance: singleProtocolFee, + it('can recover from a failed order', async () => { + const rfqOrder = createRfqOrder(); + const limitOrder = createLimitOrder(); + const bridgeOrder = createBridgeOrder(); + const fillSequence = _.shuffle([OrderType.Bridge, OrderType.Rfq, OrderType.Limit]); + const totalTakerTokens = BigNumber.sum( + rfqOrder.takerAmount, + limitOrder.takerAmount.plus(limitOrder.takerTokenFeeAmount), + bridgeOrder.takerTokenAmount, + ); + // Fail the first order in the sequence. + const failedOrderType = fillSequence[0]; + const data = createTransformData({ + fillSequence, + side: Side.Buy, + bridgeOrders: [ + { + ...bridgeOrder, + bridgeData: + failedOrderType === OrderType.Bridge + ? encodeBridgeData(REVERT_AMOUNT) + : bridgeOrder.bridgeData, + }, + ], + rfqOrders: [ + { + order: rfqOrder, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature( + failedOrderType === OrderType.Rfq ? REVERT_AMOUNT : ZERO_AMOUNT, + ), + }, + ], + limitOrders: [ + { + order: limitOrder, + maxTakerTokenFillAmount: MAX_UINT256, + signature: createOrderSignature( + failedOrderType === OrderType.Limit ? REVERT_AMOUNT : ZERO_AMOUNT, + ), + }, + ], + // Only require the last two orders to be filled. + fillAmount: BigNumber.sum(rfqOrder.makerAmount, limitOrder.makerAmount, bridgeOrder.makerTokenAmount) + .minus(failedOrderType === OrderType.Bridge ? bridgeOrder.makerTokenAmount : 0) + .minus(failedOrderType === OrderType.Rfq ? rfqOrder.makerAmount : 0) + .minus(failedOrderType === OrderType.Limit ? limitOrder.makerAmount : 0), }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee, + takerTokenBalance: totalTakerTokens, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); }); - it('can continue to the bridge order if the native order reverts', async () => { - const nativeOrders = [createOrder()]; - const bridgeOrders = [createBridgeOrder()]; - const orders = [...nativeOrders, ...bridgeOrders]; - const signatures = [ - ...nativeOrders.map(() => encodeExchangeBehavior()), // Valid Signatures - ...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures - ]; - const qfr = getExpectedSellQuoteFillResults(bridgeOrders); - await host - .executeTransform( - transformer.address, - takerToken.address, - qfr.takerAssetSpent, - sender, - taker, - encodeTransformData({ - orders, - signatures, - }), - ) - // Insufficient single protocol fee - .awaitTransactionSuccessAsync({ value: singleProtocolFee.minus(1) }); - assertBalances(await getBalancesAsync(host.address), { - ...ZERO_BALANCES, - makerAssetBalance: qfr.makerAssetBought, - protocolFeeBalance: singleProtocolFee, + it('can recover from a slipped order', async () => { + const rfqOrder = createRfqOrder(); + const limitOrder = createLimitOrder(); + const bridgeOrder = createBridgeOrder(); + const fillSequence = _.shuffle([OrderType.Bridge, OrderType.Rfq, OrderType.Limit]); + const totalTakerTokens = BigNumber.sum( + rfqOrder.takerAmount, + limitOrder.takerAmount.plus(limitOrder.takerTokenFeeAmount), + bridgeOrder.takerTokenAmount, + ); + // Slip the first order in the sequence. + const slippedOrderType = fillSequence[0]; + const data = createTransformData({ + fillSequence, + side: Side.Buy, + bridgeOrders: [ + { + ...bridgeOrder, + // If slipped, produce half the tokens. + bridgeData: + slippedOrderType === OrderType.Bridge + ? encodeBridgeData(bridgeOrder.makerTokenAmount.dividedToIntegerBy(2)) + : bridgeOrder.bridgeData, + }, + ], + rfqOrders: [ + { + order: rfqOrder, + maxTakerTokenFillAmount: MAX_UINT256, + // If slipped, set half the order to filled. + signature: createOrderSignature( + slippedOrderType === OrderType.Rfq + ? rfqOrder.takerAmount.div(2).integerValue(BigNumber.ROUND_DOWN) + : ZERO_AMOUNT, + ), + }, + ], + limitOrders: [ + { + order: limitOrder, + maxTakerTokenFillAmount: MAX_UINT256, + // If slipped, set half the order to filled. + signature: createOrderSignature( + slippedOrderType === OrderType.Limit + ? limitOrder.takerAmount.div(2).integerValue(BigNumber.ROUND_DOWN) + : ZERO_AMOUNT, + ), + }, + ], + // Only require half the first order to be filled. + fillAmount: BigNumber.sum(rfqOrder.makerAmount, limitOrder.makerAmount, bridgeOrder.makerTokenAmount) + .minus( + slippedOrderType === OrderType.Bridge + ? bridgeOrder.makerTokenAmount.div(2).integerValue(BigNumber.ROUND_UP) + : 0, + ) + .minus( + slippedOrderType === OrderType.Rfq + ? rfqOrder.makerAmount.div(2).integerValue(BigNumber.ROUND_UP) + : 0, + ) + .minus( + slippedOrderType === OrderType.Limit + ? limitOrder.makerAmount.div(2).integerValue(BigNumber.ROUND_UP) + : 0, + ), }); + const qfr = getExpectedQuoteFillResults( + data, + createSimulationState({ + ethBalance: singleProtocolFee, + takerTokenBalance: totalTakerTokens, + }), + ); + await executeTransformAsync({ + data, + takerTokenBalance: qfr.takerTokensSpent, + ethBalance: qfr.protocolFeePaid, + }); + await assertFinalBalancesAsync(qfr); }); }); }); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index e94809d01f..c7d7c4cb3c 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -7,6 +7,7 @@ export * from '../test/generated-wrappers/affiliate_fee_transformer'; export * from '../test/generated-wrappers/allowance_target'; export * from '../test/generated-wrappers/bootstrap_feature'; export * from '../test/generated-wrappers/bridge_adapter'; +export * from '../test/generated-wrappers/bridge_source'; export * from '../test/generated-wrappers/fee_collector'; export * from '../test/generated-wrappers/fee_collector_controller'; export * from '../test/generated-wrappers/fill_quote_transformer'; @@ -22,10 +23,8 @@ export * from '../test/generated-wrappers/i_bootstrap_feature'; export * from '../test/generated-wrappers/i_bridge_adapter'; export * from '../test/generated-wrappers/i_erc20_bridge'; export * from '../test/generated-wrappers/i_erc20_transformer'; -export * from '../test/generated-wrappers/i_exchange'; export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_flash_wallet'; -export * from '../test/generated-wrappers/i_gas_token'; export * from '../test/generated-wrappers/i_liquidity_provider'; export * from '../test/generated-wrappers/i_liquidity_provider_feature'; export * from '../test/generated-wrappers/i_liquidity_provider_sandbox'; @@ -51,7 +50,6 @@ export * from '../test/generated-wrappers/lib_migrate'; export * from '../test/generated-wrappers/lib_native_order'; export * from '../test/generated-wrappers/lib_native_orders_rich_errors'; export * from '../test/generated-wrappers/lib_native_orders_storage'; -export * from '../test/generated-wrappers/lib_order_hash'; export * from '../test/generated-wrappers/lib_ownable_rich_errors'; export * from '../test/generated-wrappers/lib_ownable_storage'; export * from '../test/generated-wrappers/lib_proxy_rich_errors'; @@ -71,7 +69,6 @@ export * from '../test/generated-wrappers/liquidity_provider_feature'; export * from '../test/generated-wrappers/liquidity_provider_sandbox'; export * from '../test/generated-wrappers/log_metadata_transformer'; export * from '../test/generated-wrappers/meta_transactions_feature'; -export * from '../test/generated-wrappers/mixin_adapter_addresses'; export * from '../test/generated-wrappers/mixin_balancer'; export * from '../test/generated-wrappers/mixin_bancor'; export * from '../test/generated-wrappers/mixin_co_fi_x'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 449628e30d..04edc1792a 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -35,6 +35,7 @@ "test/generated-artifacts/AllowanceTarget.json", "test/generated-artifacts/BootstrapFeature.json", "test/generated-artifacts/BridgeAdapter.json", + "test/generated-artifacts/BridgeSource.json", "test/generated-artifacts/FeeCollector.json", "test/generated-artifacts/FeeCollectorController.json", "test/generated-artifacts/FillQuoteTransformer.json", @@ -50,10 +51,8 @@ "test/generated-artifacts/IBridgeAdapter.json", "test/generated-artifacts/IERC20Bridge.json", "test/generated-artifacts/IERC20Transformer.json", - "test/generated-artifacts/IExchange.json", "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", - "test/generated-artifacts/IGasToken.json", "test/generated-artifacts/ILiquidityProvider.json", "test/generated-artifacts/ILiquidityProviderFeature.json", "test/generated-artifacts/ILiquidityProviderSandbox.json", @@ -79,7 +78,6 @@ "test/generated-artifacts/LibNativeOrder.json", "test/generated-artifacts/LibNativeOrdersRichErrors.json", "test/generated-artifacts/LibNativeOrdersStorage.json", - "test/generated-artifacts/LibOrderHash.json", "test/generated-artifacts/LibOwnableRichErrors.json", "test/generated-artifacts/LibOwnableStorage.json", "test/generated-artifacts/LibProxyRichErrors.json", @@ -99,7 +97,6 @@ "test/generated-artifacts/LiquidityProviderSandbox.json", "test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/MetaTransactionsFeature.json", - "test/generated-artifacts/MixinAdapterAddresses.json", "test/generated-artifacts/MixinBalancer.json", "test/generated-artifacts/MixinBancor.json", "test/generated-artifacts/MixinCoFiX.json", diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index d31c526163..bb774bdb4b 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -1,4 +1,29 @@ [ + { + "version": "6.0.0", + "changes": [ + { + "note": "Pull top 250 Balancer pairs on initialization", + "pr": 113 + }, + { + "note": "Support v4 `RFQ` and `Limit` orders", + "pr": 113 + }, + { + "note": "Refactor to consume latest `FillQuoteTransformer`", + "pr": 113 + }, + { + "note": "Enable `fillData` for all sources, no longer optional", + "pr": 113 + }, + { + "note": "Support `tx.origin` in RFQT quote requestor", + "pr": 113 + } + ] + }, { "version": "5.8.2", "changes": [ diff --git a/packages/asset-swapper/contracts/src/BalanceChecker.sol b/packages/asset-swapper/contracts/src/BalanceChecker.sol index 52f548fa73..bfbc970e66 100644 --- a/packages/asset-swapper/contracts/src/BalanceChecker.sol +++ b/packages/asset-swapper/contracts/src/BalanceChecker.sol @@ -58,6 +58,39 @@ contract BalanceChecker { return addrBalances; } + /* + Check the token balances of wallet-token pairs with a spender contract for an allowance check. + Pass 0xeee... as a "token" address to get ETH balance. + Possible error throws: + - extremely large arrays for user and or tokens (gas cost too high) + + Returns a one-dimensional that's user.length long. It is the lesser of balance and allowance + */ + function getMinOfBalancesOrAllowances(address[] calldata users, address[] calldata tokens, address spender) external view returns (uint256[] memory) { + // make sure the users array and tokens array are of equal length + require(users.length == tokens.length, "users array is a different length than the tokens array"); + + uint256[] memory addrBalances = new uint256[](users.length); + + for(uint i = 0; i < users.length; i++) { + if (tokens[i] != address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + uint256 balance; + uint256 allowance; + balance = IToken(tokens[i]).balanceOf(users[i]); + allowance = IToken(tokens[i]).allowance(users[i], spender); + if (allowance < balance) { + addrBalances[i] = allowance; + } else { + addrBalances[i] = balance; + } + } else { + addrBalances[i] = users[i].balance; // ETH balance + } + } + + return addrBalances; + } + /* Check the allowances of an array of owner-spender-tokens diff --git a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol index 680a54557c..a21a03940a 100644 --- a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol @@ -36,6 +36,7 @@ import "./SushiSwapSampler.sol"; import "./TwoHopSampler.sol"; import "./UniswapSampler.sol"; import "./UniswapV2Sampler.sol"; +import "./UtilitySampler.sol"; contract ERC20BridgeSampler is @@ -54,7 +55,8 @@ contract ERC20BridgeSampler is SushiSwapSampler, TwoHopSampler, UniswapSampler, - UniswapV2Sampler + UniswapV2Sampler, + UtilitySampler { struct CallResults { diff --git a/packages/asset-swapper/contracts/src/NativeOrderSampler.sol b/packages/asset-swapper/contracts/src/NativeOrderSampler.sol index d7881e57b2..43c84272fe 100644 --- a/packages/asset-swapper/contracts/src/NativeOrderSampler.sol +++ b/packages/asset-swapper/contracts/src/NativeOrderSampler.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 /* - Copyright 2020 ZeroEx Intl. + Copyright 2021 ZeroEx Intl. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,126 +28,118 @@ import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; interface IExchange { - /// @dev V3 Order structure. - struct Order { - // Address that created the order. - address makerAddress; - // Address that is allowed to fill the order. - // If set to 0, any address is allowed to fill the order. - address takerAddress; - // Address that will recieve fees when order is filled. - address feeRecipientAddress; - // Address that is allowed to call Exchange contract methods that affect this order. - // If set to 0, any address is allowed to call these methods. - address senderAddress; - // Amount of makerAsset being offered by maker. Must be greater than 0. - uint256 makerAssetAmount; - // Amount of takerAsset being bid on by maker. Must be greater than 0. - uint256 takerAssetAmount; - // Fee paid to feeRecipient by maker when order is filled. - uint256 makerFee; - // Fee paid to feeRecipient by taker when order is filled. - uint256 takerFee; - // Timestamp in seconds at which order expires. - uint256 expirationTimeSeconds; - // Arbitrary number to facilitate uniqueness of the order's hash. - uint256 salt; - // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. - // The leading bytes4 references the id of the asset proxy. - bytes makerAssetData; - // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. - // The leading bytes4 references the id of the asset proxy. - bytes takerAssetData; - // Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset. - // The leading bytes4 references the id of the asset proxy. - bytes makerFeeAssetData; - // Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset. - // The leading bytes4 references the id of the asset proxy. - bytes takerFeeAssetData; - } - - // A valid order remains fillable until it is expired, fully filled, or cancelled. - // An order's status is unaffected by external factors, like account balances. enum OrderStatus { - INVALID, // Default value - INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount - INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount - FILLABLE, // Order is fillable - EXPIRED, // Order has already expired - FULLY_FILLED, // Order is fully filled - CANCELLED // Order has been cancelled + INVALID, + FILLABLE, + FILLED, + CANCELLED, + EXPIRED } - /// @dev Order information returned by `getOrderInfo()`. + /// @dev A standard OTC or OO limit order. + struct LimitOrder { + IERC20TokenV06 makerToken; + IERC20TokenV06 takerToken; + uint128 makerAmount; + uint128 takerAmount; + uint128 takerTokenFeeAmount; + address maker; + address taker; + address sender; + address feeRecipient; + bytes32 pool; + uint64 expiry; + uint256 salt; + } + + /// @dev An RFQ limit order. + struct RfqOrder { + IERC20TokenV06 makerToken; + IERC20TokenV06 takerToken; + uint128 makerAmount; + uint128 takerAmount; + address maker; + address taker; + address txOrigin; + bytes32 pool; + uint64 expiry; + uint256 salt; + } + + /// @dev Info on a limit or RFQ order. struct OrderInfo { - OrderStatus orderStatus; // Status that describes order's validity and fillability. - bytes32 orderHash; // EIP712 typed data hash of the order (see LibOrder.getTypedDataHash). - uint256 orderTakerAssetFilledAmount; // Amount of order that has already been filled. + bytes32 orderHash; + OrderStatus status; + uint128 takerTokenFilledAmount; } - /// @dev Gets information about an order: status, hash, and amount filled. - /// @param order Order to gather information on. - /// @return orderInfo Information about the order and its state. - function getOrderInfo(IExchange.Order calldata order) + /// @dev Allowed signature types. + enum SignatureType { + ILLEGAL, + INVALID, + EIP712, + ETHSIGN + } + + /// @dev Encoded EC signature. + struct Signature { + // How to validate the signature. + SignatureType signatureType; + // EC Signature data. + uint8 v; + // EC Signature data. + bytes32 r; + // EC Signature data. + bytes32 s; + } + + /// @dev Get the order info for a limit order. + /// @param order The limit order. + /// @return orderInfo Info about the order. + function getLimitOrderInfo(LimitOrder memory order) external view - returns (IExchange.OrderInfo memory orderInfo); + returns (OrderInfo memory orderInfo); - /// @dev Verifies that a hash has been signed by the given signer. - /// @param hash Any 32-byte hash. - /// @param signature Proof that the hash has been signed by signer. - /// @return isValid `true` if the signature is valid for the given hash and signer. - function isValidHashSignature( - bytes32 hash, - address signerAddress, - bytes calldata signature + /// @dev Get order info, fillable amount, and signature validity for a limit order. + /// Fillable amount is determined using balances and allowances of the maker. + /// @param order The limit order. + /// @param signature The order signature. + /// @return orderInfo Info about the order. + /// @return actualFillableTakerTokenAmount How much of the order is fillable + /// based on maker funds, in taker tokens. + /// @return isSignatureValid Whether the signature is valid. + function getLimitOrderRelevantState( + LimitOrder memory order, + Signature calldata signature ) external view - returns (bool isValid); - - /// @dev Gets an asset proxy. - /// @param assetProxyId Id of the asset proxy. - /// @return The asset proxy registered to assetProxyId. Returns 0x0 if no proxy is registered. - function getAssetProxy(bytes4 assetProxyId) - external - view - returns (address); + returns ( + OrderInfo memory orderInfo, + uint128 actualFillableTakerTokenAmount, + bool isSignatureValid + ); } contract NativeOrderSampler { using LibSafeMathV06 for uint256; using LibBytesV06 for bytes; - /// @dev The Exchange ERC20Proxy ID. - bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0; /// @dev Gas limit for calls to `getOrderFillableTakerAmount()`. uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k - function getTokenDecimals( - address makerTokenAddress, - address takerTokenAddress - ) - public - view - returns (uint256, uint256) - { - uint256 fromTokenDecimals = LibERC20TokenV06.compatDecimals(IERC20TokenV06(makerTokenAddress)); - uint256 toTokenDecimals = LibERC20TokenV06.compatDecimals(IERC20TokenV06(takerTokenAddress)); - return (fromTokenDecimals, toTokenDecimals); - } - /// @dev Queries the fillable taker asset amounts of native orders. /// Effectively ignores orders that have empty signatures or /// maker/taker asset amounts (returning 0). - /// @param orders Native orders to query. + /// @param orders Native limit orders to query. /// @param orderSignatures Signatures for each respective order in `orders`. - /// @param exchange The V3 exchange. + /// @param exchange The V4 exchange. /// @return orderFillableTakerAssetAmounts How much taker asset can be filled /// by each order in `orders`. - function getOrderFillableTakerAssetAmounts( - IExchange.Order[] memory orders, - bytes[] memory orderSignatures, + function getLimitOrderFillableTakerAssetAmounts( + IExchange.LimitOrder[] memory orders, + IExchange.Signature[] memory orderSignatures, IExchange exchange ) public @@ -157,7 +149,7 @@ contract NativeOrderSampler { orderFillableTakerAssetAmounts = new uint256[](orders.length); for (uint256 i = 0; i != orders.length; i++) { try - this.getOrderFillableTakerAmount + this.getLimitOrderFillableTakerAmount {gas: DEFAULT_CALL_GAS} ( orders[i], @@ -178,19 +170,19 @@ contract NativeOrderSampler { /// Effectively ignores orders that have empty signatures or /// @param orders Native orders to query. /// @param orderSignatures Signatures for each respective order in `orders`. - /// @param exchange The V3 exchange. + /// @param exchange The V4 exchange. /// @return orderFillableMakerAssetAmounts How much maker asset can be filled /// by each order in `orders`. - function getOrderFillableMakerAssetAmounts( - IExchange.Order[] memory orders, - bytes[] memory orderSignatures, + function getLimitOrderFillableMakerAssetAmounts( + IExchange.LimitOrder[] memory orders, + IExchange.Signature[] memory orderSignatures, IExchange exchange ) public view returns (uint256[] memory orderFillableMakerAssetAmounts) { - orderFillableMakerAssetAmounts = getOrderFillableTakerAssetAmounts( + orderFillableMakerAssetAmounts = getLimitOrderFillableTakerAssetAmounts( orders, orderSignatures, exchange @@ -201,8 +193,8 @@ contract NativeOrderSampler { if (orderFillableMakerAssetAmounts[i] != 0) { orderFillableMakerAssetAmounts[i] = LibMathV06.getPartialAmountCeil( orderFillableMakerAssetAmounts[i], - orders[i].takerAssetAmount, - orders[i].makerAssetAmount + orders[i].takerAmount, + orders[i].makerAmount ); } } @@ -210,9 +202,9 @@ contract NativeOrderSampler { /// @dev Get the fillable taker amount of an order, taking into account /// order state, maker fees, and maker balances. - function getOrderFillableTakerAmount( - IExchange.Order memory order, - bytes memory signature, + function getLimitOrderFillableTakerAmount( + IExchange.LimitOrder memory order, + IExchange.Signature memory signature, IExchange exchange ) virtual @@ -220,88 +212,28 @@ contract NativeOrderSampler { view returns (uint256 fillableTakerAmount) { - if (signature.length == 0 || - order.makerAssetAmount == 0 || - order.takerAssetAmount == 0) + if (signature.signatureType == IExchange.SignatureType.ILLEGAL || + signature.signatureType == IExchange.SignatureType.INVALID || + order.makerAmount == 0 || + order.takerAmount == 0) { return 0; } - IExchange.OrderInfo memory orderInfo = exchange.getOrderInfo(order); - if (orderInfo.orderStatus != IExchange.OrderStatus.FILLABLE) { - return 0; - } - if (!exchange.isValidHashSignature(orderInfo.orderHash, order.makerAddress, signature)) { - return 0; - } - address spender = exchange.getAssetProxy(ERC20_ASSET_PROXY_ID); - IERC20TokenV06 makerToken = _getTokenFromERC20AssetData(order.makerAssetData); - if (makerToken == IERC20TokenV06(0)) { - return 0; - } - IERC20TokenV06 makerFeeToken = order.makerFee > 0 - ? _getTokenFromERC20AssetData(order.makerFeeAssetData) - : IERC20TokenV06(0); - uint256 remainingTakerAmount = order.takerAssetAmount - .safeSub(orderInfo.orderTakerAssetFilledAmount); - fillableTakerAmount = remainingTakerAmount; - // The total fillable maker amount is the remaining fillable maker amount - // PLUS maker fees, if maker fees are denominated in the maker token. - uint256 totalFillableMakerAmount = LibMathV06.safeGetPartialAmountFloor( - remainingTakerAmount, - order.takerAssetAmount, - makerFeeToken == makerToken - ? order.makerAssetAmount.safeAdd(order.makerFee) - : order.makerAssetAmount - ); - // The spendable amount of maker tokens (by the maker) is the lesser of - // the maker's balance and the allowance they've granted to the ERC20Proxy. - uint256 spendableMakerAmount = LibSafeMathV06.min256( - makerToken.balanceOf(order.makerAddress), - makerToken.allowance(order.makerAddress, spender) - ); - // Scale the fillable taker amount by the ratio of the maker's - // spendable maker amount over the total fillable maker amount. - if (spendableMakerAmount < totalFillableMakerAmount) { - fillableTakerAmount = LibMathV06.getPartialAmountCeil( - spendableMakerAmount, - totalFillableMakerAmount, - remainingTakerAmount - ); - } - // If the maker fee is denominated in another token, constrain - // the fillable taker amount by how much the maker can pay of that token. - if (makerFeeToken != makerToken && makerFeeToken != IERC20TokenV06(0)) { - uint256 spendableExtraMakerFeeAmount = LibSafeMathV06.min256( - makerFeeToken.balanceOf(order.makerAddress), - makerFeeToken.allowance(order.makerAddress, spender) - ); - if (spendableExtraMakerFeeAmount < order.makerFee) { - fillableTakerAmount = LibSafeMathV06.min256( - fillableTakerAmount, - LibMathV06.getPartialAmountCeil( - spendableExtraMakerFeeAmount, - order.makerFee, - remainingTakerAmount - ) - ); - } - } - } + ( + IExchange.OrderInfo memory orderInfo, + uint128 remainingFillableTakerAmount, + bool isSignatureValid + ) = exchange.getLimitOrderRelevantState(order, signature); - function _getTokenFromERC20AssetData(bytes memory assetData) - private - pure - returns (IERC20TokenV06 token) - { - if (assetData.length == 0) { - return IERC20TokenV06(address(0)); + if ( + orderInfo.status != IExchange.OrderStatus.FILLABLE || + !isSignatureValid || + order.makerToken == IERC20TokenV06(0) + ) { + return 0; } - if (assetData.length != 36 || - assetData.readBytes4(0) != ERC20_ASSET_PROXY_ID) - { - return IERC20TokenV06(address(0)); - } - return IERC20TokenV06(assetData.readAddress(16)); + + fillableTakerAmount = uint256(remainingFillableTakerAmount); } } diff --git a/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol b/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol index 59b627f978..09c9365e51 100644 --- a/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol +++ b/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol @@ -31,11 +31,13 @@ contract UniswapV2Sampler is uint256 constant private UNISWAPV2_CALL_GAS = 150e3; // 150k /// @dev Sample sell quotes from UniswapV2. + /// @param router Router to look up tokens and amounts /// @param path Token route. Should be takerToken -> makerToken /// @param takerTokenAmounts Taker token sell amount for each sample. /// @return makerTokenAmounts Maker amounts bought at each taker token /// amount. function sampleSellsFromUniswapV2( + address router, address[] memory path, uint256[] memory takerTokenAmounts ) @@ -47,7 +49,7 @@ contract UniswapV2Sampler is makerTokenAmounts = new uint256[](numSamples); for (uint256 i = 0; i < numSamples; i++) { try - IUniswapV2Router01(_getUniswapV2Router01Address()).getAmountsOut + IUniswapV2Router01(router).getAmountsOut {gas: UNISWAPV2_CALL_GAS} (takerTokenAmounts[i], path) returns (uint256[] memory amounts) @@ -61,11 +63,13 @@ contract UniswapV2Sampler is } /// @dev Sample buy quotes from UniswapV2. + /// @param router Router to look up tokens and amounts /// @param path Token route. Should be takerToken -> makerToken. /// @param makerTokenAmounts Maker token buy amount for each sample. /// @return takerTokenAmounts Taker amounts sold at each maker token /// amount. function sampleBuysFromUniswapV2( + address router, address[] memory path, uint256[] memory makerTokenAmounts ) @@ -77,7 +81,7 @@ contract UniswapV2Sampler is takerTokenAmounts = new uint256[](numSamples); for (uint256 i = 0; i < numSamples; i++) { try - IUniswapV2Router01(_getUniswapV2Router01Address()).getAmountsIn + IUniswapV2Router01(router).getAmountsIn {gas: UNISWAPV2_CALL_GAS} (makerTokenAmounts[i], path) returns (uint256[] memory amounts) diff --git a/packages/asset-swapper/contracts/src/UtilitySampler.sol b/packages/asset-swapper/contracts/src/UtilitySampler.sol new file mode 100644 index 0000000000..4e5abd9098 --- /dev/null +++ b/packages/asset-swapper/contracts/src/UtilitySampler.sol @@ -0,0 +1,80 @@ + +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; + +contract UtilitySampler { + + using LibERC20TokenV06 for IERC20TokenV06; + + IERC20TokenV06 private immutable UTILITY_ETH_ADDRESS = IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + function getTokenDecimals(IERC20TokenV06[] memory tokens) + public + view + returns (uint256[] memory decimals) + { + decimals = new uint256[](tokens.length); + for (uint256 i = 0; i != tokens.length; i++) { + decimals[i] = tokens[i] == UTILITY_ETH_ADDRESS + ? 18 + : tokens[i].compatDecimals(); + } + } + + function getBalanceOf(IERC20TokenV06[] memory tokens, address account) + public + view + returns (uint256[] memory balances) + { + balances = new uint256[](tokens.length); + for (uint256 i = 0; i != tokens.length; i++) { + balances[i] = tokens[i] == UTILITY_ETH_ADDRESS + ? account.balance + : tokens[i].compatBalanceOf(account); + } + } + + function getAllowanceOf(IERC20TokenV06[] memory tokens, address account, address spender) + public + view + returns (uint256[] memory allowances) + { + allowances = new uint256[](tokens.length); + for (uint256 i = 0; i != tokens.length; i++) { + allowances[i] = tokens[i] == UTILITY_ETH_ADDRESS + ? 0 + : tokens[i].compatAllowance(account, spender); + } + } + + function isContract(address account) + public + view + returns (bool) + { + uint256 size; + assembly { size := extcodesize(account) } + return size > 0; + } +} \ No newline at end of file diff --git a/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol b/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol index 7cad6dce75..b7ccd1c702 100644 --- a/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol @@ -480,9 +480,9 @@ contract TestERC20BridgeSampler is } // Overridden to return deterministic states. - function getOrderFillableTakerAmount( - IExchange.Order memory order, - bytes memory, + function getLimitOrderFillableTakerAmount( + IExchange.LimitOrder memory order, + IExchange.Signature memory, IExchange ) override @@ -490,7 +490,7 @@ contract TestERC20BridgeSampler is view returns (uint256 fillableTakerAmount) { - return uint256(keccak256(abi.encode(order.salt))) % order.takerAssetAmount; + return uint256(keccak256(abi.encode(order.salt))) % order.takerAmount; } // Overriden to return deterministic decimals. diff --git a/packages/asset-swapper/contracts/test/TestNativeOrderSampler.sol b/packages/asset-swapper/contracts/test/TestNativeOrderSampler.sol index fb14e37225..35a9912d6e 100644 --- a/packages/asset-swapper/contracts/test/TestNativeOrderSampler.sol +++ b/packages/asset-swapper/contracts/test/TestNativeOrderSampler.sol @@ -20,6 +20,7 @@ pragma solidity ^0.6; pragma experimental ABIEncoderV2; import "../src/NativeOrderSampler.sol"; +import "../src/UtilitySampler.sol"; contract TestNativeOrderSamplerToken { @@ -40,7 +41,8 @@ contract TestNativeOrderSamplerToken { } contract TestNativeOrderSampler is - NativeOrderSampler + NativeOrderSampler, + UtilitySampler { uint8 private constant MAX_ORDER_STATUS = uint8(IExchange.OrderStatus.CANCELLED) + 1; bytes32 private constant VALID_SIGNATURE_HASH = keccak256(hex"01"); @@ -67,42 +69,67 @@ contract TestNativeOrderSampler is token.setBalanceAndAllowance(owner, spender, balance, allowance); } - // IExchange.getAssetProxy() - function getAssetProxy(bytes4 proxyId) - public - pure - returns (address) - { - return address(uint160(uint256(keccak256(abi.encode(proxyId))))); - } - - // IExchange.getOrderInfo() - function getOrderInfo(IExchange.Order calldata order) + // IExchange.getLimitOrderRelevantState() + function getLimitOrderRelevantState( + IExchange.LimitOrder memory order, + IExchange.Signature calldata signature + ) external - pure - returns (IExchange.OrderInfo memory orderInfo) + view + returns ( + IExchange.OrderInfo memory orderInfo, + uint128 actualFillableTakerTokenAmount, + bool isSignatureValid + ) { // The order salt determines everything. orderInfo.orderHash = keccak256(abi.encode(order.salt)); if (uint8(order.salt) == 0xFF) { - orderInfo.orderStatus = IExchange.OrderStatus.FULLY_FILLED; + orderInfo.status = IExchange.OrderStatus.FILLED; } else { - orderInfo.orderStatus = IExchange.OrderStatus.FILLABLE; + orderInfo.status = IExchange.OrderStatus.FILLABLE; } + + isSignatureValid = signature.r == VALID_SIGNATURE_HASH; + // The expiration time is the filled taker asset amount. - orderInfo.orderTakerAssetFilledAmount = order.expirationTimeSeconds; + orderInfo.takerTokenFilledAmount = uint128(order.expiry); + + // Calculate how much is fillable in maker terms given the filled taker amount + uint256 fillableMakerTokenAmount = LibMathV06.getPartialAmountFloor( + uint256( + order.takerAmount + - orderInfo.takerTokenFilledAmount + ), + uint256(order.takerAmount), + uint256(order.makerAmount) + ); + + // Take the min of the balance/allowance and the fillable maker amount + fillableMakerTokenAmount = LibSafeMathV06.min256( + fillableMakerTokenAmount, + _getSpendableERC20BalanceOf(order.makerToken, order.maker) + ); + + // Convert to taker terms + actualFillableTakerTokenAmount = LibMathV06.getPartialAmountCeil( + fillableMakerTokenAmount, + uint256(order.makerAmount), + uint256(order.takerAmount) + ).safeDowncastToUint128(); } - // IExchange.isValidSignature() - function isValidHashSignature( - bytes32, - address, - bytes calldata signature + function _getSpendableERC20BalanceOf( + IERC20TokenV06 token, + address owner ) - external - pure - returns (bool isValid) + internal + view + returns (uint256) { - return keccak256(signature) == VALID_SIGNATURE_HASH; + return LibSafeMathV06.min256( + token.allowance(owner, address(this)), + token.balanceOf(owner) + ); } } diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 971b832e96..e59fa69b6b 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BancorSampler|CurveSampler|DODOSampler|DeploymentConstants|DummyLiquidityProvider|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|IBancor|ICurve|IEth2Dai|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json", + "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BancorSampler|CurveSampler|DODOSampler|DeploymentConstants|DummyLiquidityProvider|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|IBancor|ICurve|IEth2Dai|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UtilitySampler).json", "postpublish": { "assets": [] } @@ -63,8 +63,7 @@ "@0x/contract-wrappers": "^13.12.2", "@0x/dev-utils": "^4.2.1", "@0x/json-schemas": "^5.4.1", - "@0x/order-utils": "^10.4.15", - "@0x/orderbook": "0xProject/gitpkg-registry#0x-orderbook-v2.2.7-e10a81023", + "@0x/protocol-utils": "^1.1.3", "@0x/quote-server": "^4.0.1", "@0x/types": "^3.3.1", "@0x/typescript-typings": "^5.1.6", diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index 9c4e10d4bf..b243c016aa 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -1,15 +1,13 @@ import { ChainId } from '@0x/contract-addresses'; +import { SignatureType } from '@0x/protocol-utils'; import { BigNumber, logUtils } from '@0x/utils'; import { ExchangeProxyContractOpts, - ExtensionContractType, - ForwarderExtensionContractOpts, LogFunction, OrderPrunerOpts, OrderPrunerPermittedFeeTypes, RfqtRequestOpts, - SwapQuoteExecutionOpts, SwapQuoteGetOutputOpts, SwapQuoteRequestOpts, SwapQuoterOpts, @@ -29,10 +27,7 @@ const ZERO_AMOUNT = new BigNumber(0); const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = { expiryBufferMs: 120000, // 2 minutes - permittedOrderFeeTypes: new Set([ - OrderPrunerPermittedFeeTypes.NoFees, - OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee, - ]), // Default asset-swapper for CFL oriented fee types + permittedOrderFeeTypes: new Set([OrderPrunerPermittedFeeTypes.NoFees]), // Default asset-swapper for CFL oriented fee types }; // 6 seconds polling interval @@ -55,16 +50,6 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { }, }; -const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = { - feePercentage: 0, - feeRecipient: NULL_ADDRESS, -}; - -const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = { - useExtensionContract: ExtensionContractType.Forwarder, - extensionContractOpts: DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS, -}; - const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = { isFromETH: false, isToETH: false, @@ -78,10 +63,7 @@ const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts shouldSellEntireBalance: false, }; -const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS; - const DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = { - useExtensionContract: ExtensionContractType.ExchangeProxy, extensionContractOpts: DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS, }; @@ -91,10 +73,6 @@ const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = { const DEFAULT_RFQT_REQUEST_OPTS: Partial = { makerEndpointMaxResponseTimeMs: 1000, - priceAwareRFQFlag: { - isFirmPriceAwareEnabled: false, - isIndicativePriceAwareEnabled: false, - }, }; export const DEFAULT_INFO_LOGGER: LogFunction = (obj, msg) => @@ -102,14 +80,8 @@ export const DEFAULT_INFO_LOGGER: LogFunction = (obj, msg) => export const DEFAULT_WARNING_LOGGER: LogFunction = (obj, msg) => logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`); -// This feature flag allows us to merge the price-aware RFQ pricing -// project while still controlling when to activate the feature. We plan to do some -// data analysis work and address some of the issues with maker fillable amounts -// in later milestones. Once the feature is fully rolled out and is providing value -// and we have assessed that there is no user impact, we will proceed in cleaning up -// the feature flag. When that time comes, follow this PR to "undo" the feature flag: -// https://github.com/0xProject/0x-monorepo/pull/2735 -export const IS_PRICE_AWARE_RFQ_ENABLED: boolean = false; +const EMPTY_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; +export const INVALID_SIGNATURE = { signatureType: SignatureType.Invalid, v: 1, r: EMPTY_BYTES32, s: EMPTY_BYTES32 }; export { BRIDGE_ADDRESSES_BY_CHAIN, @@ -131,8 +103,6 @@ export const constants = { ONE_MINUTE_MS, DEFAULT_SWAP_QUOTER_OPTS, DEFAULT_INTERMEDIATE_TOKENS, - DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, - DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, DEFAULT_SWAP_QUOTE_REQUEST_OPTS, DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS, DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS, @@ -144,4 +114,5 @@ export const constants = { BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3', DEFAULT_INFO_LOGGER, DEFAULT_WARNING_LOGGER, + EMPTY_BYTES32, }; diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 8e24ce7b69..013aea09c1 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -5,30 +5,18 @@ export { SendTransactionOpts, } from '@0x/base-contract'; export { ContractAddresses } from '@0x/contract-addresses'; -export { WSOpts } from '@0x/mesh-rpc-client'; -export { - AcceptedRejectedOrders, - AddedRemovedOrders, - BaseOrderProvider, - MeshOrderProviderOpts, - Orderbook, - OrderSet, - OrderStore, - RejectedOrder, - SRAPollingOrderProviderOpts, - SRAWebsocketOrderProviderOpts, -} from '@0x/orderbook'; -export { V3RFQFirmQuote, V3RFQIndicativeQuote, TakerRequestQueryParams } from '@0x/quote-server'; -export { - APIOrder, - Asset, - AssetPairsItem, - DecodedLogEvent, - EventCallback, - IndexedFilterValues, - SignedOrder, -} from '@0x/types'; +export { V4RFQFirmQuote, V4RFQIndicativeQuote, V4SignedRfqOrder, TakerRequestQueryParams } from '@0x/quote-server'; +export { Asset, AssetPairsItem, DecodedLogEvent, EventCallback, IndexedFilterValues } from '@0x/types'; export { BigNumber } from '@0x/utils'; +export { + RfqOrderFields, + LimitOrderFields, + FillQuoteTransformerOrderType, + RfqOrder, + LimitOrder, + Signature, + SignatureType, +} from '@0x/protocol-utils'; export { AxiosInstance } from 'axios'; export { AbiDefinition, @@ -84,29 +72,26 @@ export { export { artifacts } from './artifacts'; export { InsufficientAssetLiquidityError } from './errors'; export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer'; -export { getSwapMinBuyAmount, getQuoteInfoMinBuyAmount } from './quote_consumers/utils'; -export { SwapQuoter } from './swap_quoter'; +export { SwapQuoter, Orderbook } from './swap_quoter'; export { AffiliateFee, AssetSwapperContractAddresses, CalldataInfo, ExchangeProxyContractOpts, ExchangeProxyRefundReceiver, - ExtensionContractType, - ForwarderExtensionContractOpts, GetExtensionContractTypeOpts, - LiquidityForTakerMakerAssetDataPair, LogFunction, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote, - MockedRfqtFirmQuoteResponse, + MockedRfqtQuoteResponse, OrderPrunerPermittedFeeTypes, RfqtMakerAssetOfferings, RfqtFirmQuoteValidator, RfqtRequestOpts, SamplerOverrides, - SignedOrderWithFillableAmounts, + SignedNativeOrder, + SignedOrder, SwapQuote, SwapQuoteConsumerBase, SwapQuoteConsumerError, @@ -154,11 +139,12 @@ export { MooniswapFillData, MultiHopFillData, NativeCollapsedFill, + NativeRfqOrderFillData, + NativeLimitOrderFillData, NativeFillData, OptimizedMarketOrder, SnowSwapFillData, SnowSwapInfo, - SourceInfo, SourceQuoteOperation, SushiSwapFillData, SwerveFillData, @@ -168,16 +154,17 @@ export { } from './utils/market_operation_utils/types'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { - BridgeReportSource, - MultiHopReportSource, - NativeOrderbookReportSource, - NativeRFQTReportSource, + BridgeQuoteReportEntry, + MultiHopQuoteReportEntry, + NativeLimitOrderQuoteReportEntry, + NativeRfqOrderQuoteReportEntry, QuoteReport, - QuoteReportSource, + QuoteReportEntry, } from './utils/quote_report_generator'; export { QuoteRequestor } from './utils/quote_requestor'; -export { rfqtMocker } from './utils/rfqt_mocker'; export { ERC20BridgeSamplerContract, BalanceCheckerContract } from './wrappers'; import { ERC20BridgeSource } from './utils/market_operation_utils/types'; export type Native = ERC20BridgeSource.Native; export type MultiHop = ERC20BridgeSource.MultiHop; + +export { rfqtMocker, RfqtQuoteEndpoint } from './utils/rfqt_mocker'; diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index d85689f512..b1c757429f 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -6,9 +6,11 @@ import { encodePayTakerTransformerData, encodeWethTransformerData, ETH_TOKEN_ADDRESS, + FillQuoteTransformerData, + FillQuoteTransformerOrderType, FillQuoteTransformerSide, findTransformerNonce, -} from '@0x/order-utils'; +} from '@0x/protocol-utils'; import { BigNumber, providerUtils } from '@0x/utils'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import * as _ from 'lodash'; @@ -27,10 +29,20 @@ import { SwapQuoteGetOutputOpts, } from '../types'; import { assert } from '../utils/assert'; -import { ERC20BridgeSource, UniswapV2FillData } from '../utils/market_operation_utils/types'; -import { getTokenFromAssetData } from '../utils/utils'; - -import { getSwapMinBuyAmount } from './utils'; +import { + createBridgeDataForBridgeOrder, + getERC20BridgeSourceToBridgeSource, +} from '../utils/market_operation_utils/orders'; +import { + ERC20BridgeSource, + LiquidityProviderFillData, + NativeLimitOrderFillData, + NativeRfqOrderFillData, + OptimizedMarketBridgeOrder, + OptimizedMarketOrder, + OptimizedMarketOrderBase, + UniswapV2FillData, +} from '../utils/market_operation_utils/types'; // tslint:disable-next-line:custom-no-magic-numbers const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); @@ -84,7 +96,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { quote: MarketBuySwapQuote | MarketSellSwapQuote, opts: Partial = {}, ): Promise { - assert.isValidSwapQuote('quote', quote); const optsWithDefaults: ExchangeProxyContractOpts = { ...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS, ...opts.extensionContractOpts, @@ -92,11 +103,14 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { // tslint:disable-next-line:no-object-literal-type-assertion const { refundReceiver, affiliateFee, isFromETH, isToETH, shouldSellEntireBalance } = optsWithDefaults; - const sellToken = getTokenFromAssetData(quote.takerAssetData); - const buyToken = getTokenFromAssetData(quote.makerAssetData); - const sellAmount = quote.worstCaseQuoteInfo.totalTakerAssetAmount; - let minBuyAmount = getSwapMinBuyAmount(quote); + const sellToken = quote.takerToken; + const buyToken = quote.makerToken; + + // Take the bounds from the worst case + const sellAmount = quote.worstCaseQuoteInfo.totalTakerAmount; + let minBuyAmount = quote.worstCaseQuoteInfo.makerAmount; let ethAmount = quote.worstCaseQuoteInfo.protocolFeeInWeiAmount; + if (isFromETH) { ethAmount = ethAmount.plus(sellAmount); } @@ -106,8 +120,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { if ( isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.SushiSwap]) ) { - const source = quote.orders[0].fills[0].source; - const fillData = quote.orders[0].fills[0].fillData as UniswapV2FillData; + const source = quote.orders[0].source; + const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder).fillData; return { calldataHexString: this._exchangeProxy .sellToUniswap( @@ -132,7 +146,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { } if (isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.LiquidityProvider])) { - const target = quote.orders[0].makerAddress; + const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder).fillData; + const target = fillData.poolAddress; return { calldataHexString: this._exchangeProxy .sellToLiquidityProvider( @@ -164,52 +179,46 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { }); } - const intermediateToken = quote.isTwoHop ? getTokenFromAssetData(quote.orders[0].makerAssetData) : NULL_ADDRESS; + // If it's two hop we have an intermediate token this is needed to encode the individual FQT + // and we also want to ensure no dust amount is left in the flash wallet + const intermediateToken = quote.isTwoHop ? quote.orders[0].makerToken : NULL_ADDRESS; // This transformer will fill the quote. if (quote.isTwoHop) { const [firstHopOrder, secondHopOrder] = quote.orders; transforms.push({ deploymentNonce: this.transformerNonces.fillQuoteTransformer, data: encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, sellToken, buyToken: intermediateToken, - side: FillQuoteTransformerSide.Sell, + ...getFQTTransformerDataFromOptimizedOrders([firstHopOrder]), refundReceiver: refundReceiver || NULL_ADDRESS, - fillAmount: shouldSellEntireBalance ? MAX_UINT256 : firstHopOrder.takerAssetAmount, - maxOrderFillAmounts: [], - rfqtTakerAddress: NULL_ADDRESS, - orders: [firstHopOrder], - signatures: [firstHopOrder.signature], + fillAmount: shouldSellEntireBalance ? MAX_UINT256 : firstHopOrder.takerAmount, }), }); transforms.push({ deploymentNonce: this.transformerNonces.fillQuoteTransformer, data: encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, buyToken, sellToken: intermediateToken, + ...getFQTTransformerDataFromOptimizedOrders([secondHopOrder]), refundReceiver: refundReceiver || NULL_ADDRESS, - side: FillQuoteTransformerSide.Sell, fillAmount: MAX_UINT256, - maxOrderFillAmounts: [], - rfqtTakerAddress: NULL_ADDRESS, - orders: [secondHopOrder], - signatures: [secondHopOrder.signature], }), }); } else { - const fillAmount = isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount; + const fillAmount = isBuyQuote(quote) ? quote.makerTokenFillAmount : quote.takerTokenFillAmount; + transforms.push({ deploymentNonce: this.transformerNonces.fillQuoteTransformer, data: encodeFillQuoteTransformerData({ + side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, sellToken, buyToken, + ...getFQTTransformerDataFromOptimizedOrders(quote.orders), refundReceiver: refundReceiver || NULL_ADDRESS, - side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, fillAmount: !isBuyQuote(quote) && shouldSellEntireBalance ? MAX_UINT256 : fillAmount, - maxOrderFillAmounts: [], - rfqtTakerAddress: NULL_ADDRESS, - orders: quote.orders, - signatures: quote.orders.map(o => o.signature), }), }); } @@ -282,10 +291,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { } } -function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { - return quote.type === MarketOperation.Buy; -} - function isDirectSwapCompatible( quote: SwapQuote, opts: ExchangeProxyContractOpts, @@ -304,12 +309,7 @@ function isDirectSwapCompatible( return false; } const order = quote.orders[0]; - // With a single underlying fill/source. - if (order.fills.length !== 1) { - return false; - } - const fill = order.fills[0]; - if (!directSources.includes(fill.source)) { + if (!directSources.includes(order.source)) { return false; } // VIP does not support selling the entire balance @@ -318,3 +318,58 @@ function isDirectSwapCompatible( } return true; } + +function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { + return quote.type === MarketOperation.Buy; +} + +function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder { + return x.type === FillQuoteTransformerOrderType.Bridge; +} + +function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase { + return x.type === FillQuoteTransformerOrderType.Limit; +} + +function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase { + return x.type === FillQuoteTransformerOrderType.Rfq; +} + +function getFQTTransformerDataFromOptimizedOrders( + orders: OptimizedMarketOrder[], +): Pick { + const fqtData: Pick = { + bridgeOrders: [], + limitOrders: [], + rfqOrders: [], + fillSequence: [], + }; + + for (const order of orders) { + if (isOptimizedBridgeOrder(order)) { + fqtData.bridgeOrders.push({ + bridgeData: createBridgeDataForBridgeOrder(order), + makerTokenAmount: order.makerAmount, + takerTokenAmount: order.takerAmount, + source: getERC20BridgeSourceToBridgeSource(order.source), + }); + } else if (isOptimizedLimitOrder(order)) { + fqtData.limitOrders.push({ + order: order.fillData.order, + signature: order.fillData.signature, + maxTakerTokenFillAmount: order.takerAmount, + }); + } else if (isOptimizedRfqOrder(order)) { + fqtData.rfqOrders.push({ + order: order.fillData.order, + signature: order.fillData.signature, + maxTakerTokenFillAmount: order.takerAmount, + }); + } else { + // Should never happen + throw new Error('Unknown Order type'); + } + fqtData.fillSequence.push(order.type); + } + return fqtData; +} diff --git a/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts index 8a5bea1691..611853dbe2 100644 --- a/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts @@ -6,8 +6,6 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { CalldataInfo, - ExtensionContractType, - GetExtensionContractTypeOpts, SwapQuote, SwapQuoteConsumerBase, SwapQuoteConsumerOpts, @@ -15,7 +13,6 @@ import { SwapQuoteGetOutputOpts, } from '../types'; import { assert } from '../utils/assert'; -import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer'; @@ -57,7 +54,6 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { quote: SwapQuote, opts: Partial = {}, ): Promise { - assert.isValidSwapQuote('quote', quote); const consumer = await this._getConsumerForSwapQuoteAsync(opts); return consumer.getCalldataOrThrowAsync(quote, opts); } @@ -71,35 +67,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { quote: SwapQuote, opts: Partial = {}, ): Promise { - assert.isValidSwapQuote('quote', quote); const consumer = await this._getConsumerForSwapQuoteAsync(opts); return consumer.executeSwapQuoteOrThrowAsync(quote, opts); } - /** - * Given a SwapQuote, returns optimal 0x protocol interface (extension or no extension) to perform the swap. - * @param quote An object that conforms to SwapQuote. See type definition for more information. - * @param opts Options for getting optimal exteion contract to fill quote. See type definition for more information. - */ - public async getOptimalExtensionContractTypeAsync( - quote: SwapQuote, - opts: Partial = {}, - ): Promise { - return swapQuoteConsumerUtils.getExtensionContractTypeForSwapQuoteAsync( - quote, - this._contractAddresses, - this.provider, - opts, - ); - } - private async _getConsumerForSwapQuoteAsync(opts: Partial): Promise { - // ( akroeger)leaving this switch to use different contracts in the future - switch (opts.useExtensionContract) { - case ExtensionContractType.ExchangeProxy: - return this._exchangeProxyConsumer; - default: - return this._exchangeProxyConsumer; - } + return this._exchangeProxyConsumer; } } diff --git a/packages/asset-swapper/src/quote_consumers/utils.ts b/packages/asset-swapper/src/quote_consumers/utils.ts deleted file mode 100644 index e257c061e2..0000000000 --- a/packages/asset-swapper/src/quote_consumers/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; - -import { MarketOperation, SwapQuote, SwapQuoteInfo } from '../types'; -import { ERC20BridgeSource, OptimizedMarketOrder } from '../utils/market_operation_utils/types'; - -/** - * Compute the minimum buy token amount for market operations by inferring - * the slippage from the orders in a quote. We cannot rely on - * `worstCaseQuoteInfo.makerAssetAmount` because that does not stop at - * maximum slippage. - */ -export function getSwapMinBuyAmount(quote: SwapQuote): BigNumber { - if (quote.type === MarketOperation.Buy || quote.isTwoHop) { - return quote.worstCaseQuoteInfo.makerAssetAmount; - } - let slipRatio = new BigNumber(1); - // Infer the allowed maker asset slippage from any non-native order. - for (const o of quote.orders) { - if (o.fills.length === 0 || o.fills[0].source === ERC20BridgeSource.Native) { - // No slippage on native orders. - continue; - } - const totalFillMakerAssetAmount = BigNumber.sum(...o.fills.map(f => f.output)); - slipRatio = o.fillableMakerAssetAmount.div(totalFillMakerAssetAmount); - break; - } - if (slipRatio.gte(1)) { - // No slippage allowed across all orders. - return quote.bestCaseQuoteInfo.makerAssetAmount; - } - return quote.bestCaseQuoteInfo.makerAssetAmount.times(slipRatio).integerValue(BigNumber.ROUND_DOWN); -} - -/** - * Same as `getSwapMinBuyAmount` but operates - * on a single quote info instead of using best and worst case - * Orders must be derived from the same path as the quote info - */ -export function getQuoteInfoMinBuyAmount( - quoteInfo: SwapQuoteInfo, - orders: OptimizedMarketOrder[], - marketOperation: MarketOperation, -): BigNumber { - if (marketOperation === MarketOperation.Buy) { - return quoteInfo.makerAssetAmount; - } - let slipRatio = new BigNumber(1); - // Infer the allowed maker asset slippage from any non-native order. - for (const o of orders) { - if (o.fills.length === 0 || o.fills[0].source === ERC20BridgeSource.Native) { - // No slippage on native orders. - continue; - } - const totalFillMakerAssetAmount = BigNumber.sum(...o.fills.map(f => f.output)); - slipRatio = o.fillableMakerAssetAmount.div(totalFillMakerAssetAmount); - break; - } - if (slipRatio.gte(1)) { - // No slippage allowed across all orders. - return quoteInfo.makerAssetAmount; - } - return quoteInfo.makerAssetAmount.times(slipRatio).integerValue(BigNumber.ROUND_DOWN); -} diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 729a463f94..5f34ba64f9 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -1,50 +1,65 @@ import { ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; -import { DevUtilsContract } from '@0x/contract-wrappers'; -import { schemas } from '@0x/json-schemas'; -import { assetDataUtils, SignedOrder } from '@0x/order-utils'; -import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook'; +import { FillQuoteTransformerOrderType, LimitOrder } from '@0x/protocol-utils'; import { BigNumber, providerUtils } from '@0x/utils'; import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts } from './artifacts'; -import { BRIDGE_ADDRESSES_BY_CHAIN, constants } from './constants'; +import { BRIDGE_ADDRESSES_BY_CHAIN, constants, INVALID_SIGNATURE } from './constants'; import { AssetSwapperContractAddresses, - CalculateSwapQuoteOpts, - LiquidityForTakerMakerAssetDataPair, MarketBuySwapQuote, MarketOperation, - MarketSellSwapQuote, OrderPrunerPermittedFeeTypes, - SignedOrderWithFillableAmounts, + RfqtRequestOpts, + SignedNativeOrder, SwapQuote, + SwapQuoteInfo, + SwapQuoteOrdersBreakdown, SwapQuoteRequestOpts, SwapQuoterOpts, SwapQuoterRfqtOpts, } from './types'; import { assert } from './utils/assert'; -import { calculateLiquidity } from './utils/calculate_liquidity'; import { MarketOperationUtils } from './utils/market_operation_utils'; import { BancorService } from './utils/market_operation_utils/bancor_service'; -import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; +import { SOURCE_FLAGS, ZERO_AMOUNT } from './utils/market_operation_utils/constants'; import { DexOrderSampler } from './utils/market_operation_utils/sampler'; import { SourceFilters } from './utils/market_operation_utils/source_filters'; import { ERC20BridgeSource, + FeeSchedule, + FillData, + GetMarketOrdersOpts, MarketDepth, MarketDepthSide, MarketSideLiquidity, + OptimizedMarketOrder, + OptimizerResultWithReport, } from './utils/market_operation_utils/types'; -import { orderPrunerUtils } from './utils/order_prune_utils'; -import { OrderStateUtils } from './utils/order_state_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; import { QuoteRequestor } from './utils/quote_requestor'; -import { sortingUtils } from './utils/sorting_utils'; -import { SwapQuoteCalculator } from './utils/swap_quote_calculator'; -import { getPriceAwareRFQRolloutFlags } from './utils/utils'; +import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './utils/quote_simulation'; import { ERC20BridgeSamplerContract } from './wrappers'; +export abstract class Orderbook { + public abstract getOrdersAsync( + makerToken: string, + takerToken: string, + pruneFn?: (o: SignedNativeOrder) => boolean, + ): Promise; + public abstract getBatchOrdersAsync( + makerTokens: string[], + takerToken: string, + pruneFn?: (o: SignedNativeOrder) => boolean, + ): Promise; + // tslint:disable-next-line:prefer-function-over-method + public async destroyAsync(): Promise { + return; + } +} + +// tslint:disable:max-classes-per-file export class SwapQuoter { public readonly provider: ZeroExProvider; public readonly orderbook: Orderbook; @@ -53,105 +68,9 @@ export class SwapQuoter { public readonly permittedOrderFeeTypes: Set; private readonly _contractAddresses: AssetSwapperContractAddresses; private readonly _protocolFeeUtils: ProtocolFeeUtils; - private readonly _swapQuoteCalculator: SwapQuoteCalculator; - private readonly _devUtilsContract: DevUtilsContract; private readonly _marketOperationUtils: MarketOperationUtils; - private readonly _orderStateUtils: OrderStateUtils; private readonly _rfqtOptions?: SwapQuoterRfqtOpts; - /** - * Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. - * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. - * @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData. - * @param options Initialization options for the SwapQuoter. See type definition for details. - * - * @return An instance of SwapQuoter - */ - public static getSwapQuoterForProvidedOrders( - supportedProvider: SupportedProvider, - orders: SignedOrder[], - options: Partial = {}, - ): SwapQuoter { - assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); - assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); - const orderbook = Orderbook.getOrderbookForProvidedOrders(orders); - const swapQuoter = new SwapQuoter(supportedProvider, orderbook, options); - return swapQuoter; - } - - /** - * Instantiates a new SwapQuoter instance given a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint - * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. - * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. - * @param options Initialization options for the SwapQuoter. See type definition for details. - * - * @return An instance of SwapQuoter - */ - public static getSwapQuoterForStandardRelayerAPIUrl( - supportedProvider: SupportedProvider, - sraApiUrl: string, - options: Partial = {}, - ): SwapQuoter { - const provider = providerUtils.standardizeOrThrow(supportedProvider); - assert.isWebUri('sraApiUrl', sraApiUrl); - const orderbook = Orderbook.getOrderbookForPollingProvider({ - httpEndpoint: sraApiUrl, - pollingIntervalMs: - options.orderRefreshIntervalMs || constants.DEFAULT_SWAP_QUOTER_OPTS.orderRefreshIntervalMs, - perPage: options.perPage || constants.DEFAULT_PER_PAGE, - }); - const swapQuoter = new SwapQuoter(provider, orderbook, options); - return swapQuoter; - } - /** - * Instantiates a new SwapQuoter instance given a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint - * and a websocket endpoint. This is more effecient than `getSwapQuoterForStandardRelayerAPIUrl` when requesting multiple quotes. - * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. - * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. - * @param sraWebsocketApiUrl The standard relayer API Websocket url you would like to subscribe to. - * @param options Initialization options for the SwapQuoter. See type definition for details. - * - * @return An instance of SwapQuoter - */ - public static getSwapQuoterForStandardRelayerAPIWebsocket( - supportedProvider: SupportedProvider, - sraApiUrl: string, - sraWebsocketAPIUrl: string, - options: Partial = {}, - ): SwapQuoter { - const provider = providerUtils.standardizeOrThrow(supportedProvider); - assert.isWebUri('sraApiUrl', sraApiUrl); - assert.isUri('sraWebsocketAPIUrl', sraWebsocketAPIUrl); - const orderbook = Orderbook.getOrderbookForWebsocketProvider({ - httpEndpoint: sraApiUrl, - websocketEndpoint: sraWebsocketAPIUrl, - }); - const swapQuoter = new SwapQuoter(provider, orderbook, options); - return swapQuoter; - } - /** - * Instantiates a new SwapQuoter instance given a 0x Mesh endpoint. This pulls all available liquidity stored in Mesh - * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. - * @param meshEndpoint The standard relayer API base HTTP url you would like to source orders from. - * @param options Initialization options for the SwapQuoter. See type definition for details. - * - * @return An instance of SwapQuoter - */ - public static getSwapQuoterForMeshEndpoint( - supportedProvider: SupportedProvider, - meshEndpoint: string, - options: Partial = {}, - ): SwapQuoter { - const provider = providerUtils.standardizeOrThrow(supportedProvider); - assert.isUri('meshEndpoint', meshEndpoint); - const orderbook = Orderbook.getOrderbookForMeshProvider({ - websocketEndpoint: meshEndpoint, - wsOpts: options.wsOpts, - }); - const swapQuoter = new SwapQuoter(provider, orderbook, options); - return swapQuoter; - } - /** * Instantiates a new SwapQuoter instance * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. @@ -185,12 +104,10 @@ export class SwapQuoter { ...getContractAddressesForChainOrThrow(chainId), ...BRIDGE_ADDRESSES_BY_CHAIN[chainId], }; - this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._protocolFeeUtils = ProtocolFeeUtils.getInstance( constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, options.ethGasStationUrl, ); - this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); // Allow the sampler bytecode to be overwritten using geths override functionality const samplerBytecode = _.get(artifacts.ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object'); const defaultCodeOverrides = samplerBytecode @@ -228,70 +145,16 @@ export class SwapQuoter { exchangeAddress: this._contractAddresses.exchange, }, ); - this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils); } - /** - * Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address. - * You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption. - * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetSellAmount The amount of taker asset to swap for. - * @param options Options for the request. See type definition for more information. - * - * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. - */ - public async getMarketSellSwapQuoteForAssetDataAsync( - makerAssetData: string, - takerAssetData: string, - takerAssetSellAmount: BigNumber, + public async getBatchMarketBuySwapQuoteAsync( + makerTokens: string[], + targetTakerToken: string, + makerTokenBuyAmounts: BigNumber[], options: Partial = {}, - ): Promise { - assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount); - return (await this._getSwapQuoteAsync( - makerAssetData, - takerAssetData, - takerAssetSellAmount, - MarketOperation.Sell, - options, - )) as MarketSellSwapQuote; - } - - /** - * Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address. - * You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption. - * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param makerAssetBuyAmount The amount of maker asset to swap for. - * @param options Options for the request. See type definition for more information. - * - * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. - */ - public async getMarketBuySwapQuoteForAssetDataAsync( - makerAssetData: string, - takerAssetData: string, - makerAssetBuyAmount: BigNumber, - options: Partial = {}, - ): Promise { - assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount); - return (await this._getSwapQuoteAsync( - makerAssetData, - takerAssetData, - makerAssetBuyAmount, - MarketOperation.Buy, - options, - )) as MarketBuySwapQuote; - } - - public async getBatchMarketBuySwapQuoteForAssetDataAsync( - makerAssetDatas: string[], - takerAssetData: string, - makerAssetBuyAmount: BigNumber[], - options: Partial = {}, - ): Promise> { - makerAssetBuyAmount.map((a, i) => assert.isBigNumber(`makerAssetBuyAmount[${i}]`, a)); + ): Promise { + makerTokenBuyAmounts.map((a, i) => assert.isBigNumber(`makerAssetBuyAmounts[${i}]`, a)); let gasPrice: BigNumber; - const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options); if (!!options.gasPrice) { gasPrice = options.gasPrice; assert.isBigNumber('gasPrice', gasPrice); @@ -299,122 +162,46 @@ export class SwapQuoter { gasPrice = await this.getGasPriceEstimationOrThrowAsync(); } - const apiOrders = await this.orderbook.getBatchOrdersAsync(makerAssetDatas, [takerAssetData]); - const allOrders = apiOrders.map(orders => orders.map(o => o.order)); - const allPrunedOrders = allOrders.map((orders, i) => { - const prunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - this.permittedOrderFeeTypes, - this.expiryBufferMs, - ); - if (prunedOrders.length === 0) { - return [ - createDummyOrderForSampler( - makerAssetDatas[i], - takerAssetData, - this._contractAddresses.uniswapBridge, - ), - ]; - } else { - return sortingUtils.sortOrders(prunedOrders); + const allOrders = await this.orderbook.getBatchOrdersAsync( + makerTokens, + targetTakerToken, + this._limitOrderPruningFn, + ); + + // Orders could be missing from the orderbook, so we create a dummy one as a placeholder + allOrders.forEach((orders: SignedNativeOrder[], i: number) => { + if (!orders || orders.length === 0) { + allOrders[i] = [createDummyOrder(makerTokens[i], targetTakerToken)]; } }); - const swapQuotes = await this._swapQuoteCalculator.calculateBatchMarketBuySwapQuoteAsync( - allPrunedOrders, - makerAssetBuyAmount, - gasPrice, - calculateSwapQuoteOpts, + const opts = { ...constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, ...options }; + const optimizerResults = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync( + allOrders, + makerTokenBuyAmounts, + opts as GetMarketOrdersOpts, ); - return swapQuotes; - } - /** - * Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address. - * You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption. - * @param makerTokenAddress The address of the maker asset - * @param takerTokenAddress The address of the taker asset - * @param makerAssetBuyAmount The amount of maker asset to swap for. - * @param options Options for the request. See type definition for more information. - * - * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. - */ - public async getMarketBuySwapQuoteAsync( - makerTokenAddress: string, - takerTokenAddress: string, - makerAssetBuyAmount: BigNumber, - options: Partial = {}, - ): Promise { - assert.isETHAddressHex('makerTokenAddress', makerTokenAddress); - assert.isETHAddressHex('takerTokenAddress', takerTokenAddress); - assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount); - const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); - const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); - return this.getMarketBuySwapQuoteForAssetDataAsync( - makerAssetData, - takerAssetData, - makerAssetBuyAmount, - options, - ); - } - /** - * Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address. - * You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption. - * @param makerTokenAddress The address of the maker asset - * @param takerTokenAddress The address of the taker asset - * @param takerAssetSellAmount The amount of taker asset to sell. - * @param options Options for the request. See type definition for more information. - * - * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. - */ - public async getMarketSellSwapQuoteAsync( - makerTokenAddress: string, - takerTokenAddress: string, - takerAssetSellAmount: BigNumber, - options: Partial = {}, - ): Promise { - assert.isETHAddressHex('makerTokenAddress', makerTokenAddress); - assert.isETHAddressHex('takerTokenAddress', takerTokenAddress); - assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount); - const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); - const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); - return this.getMarketSellSwapQuoteForAssetDataAsync( - makerAssetData, - takerAssetData, - takerAssetSellAmount, - options, + const batchSwapQuotes = await Promise.all( + optimizerResults.map(async (result, i) => { + if (result) { + const { makerToken, takerToken } = allOrders[i][0].order; + return createSwapQuote( + result, + makerToken, + takerToken, + MarketOperation.Buy, + makerTokenBuyAmounts[i], + gasPrice, + opts.gasSchedule, + opts.bridgeSlippage, + ); + } else { + return undefined; + } + }), ); - } - - /** - * Returns information about available liquidity for an asset - * Does not factor in slippage or fees - * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * - * @return An object that conforms to LiquidityForTakerMakerAssetDataPair that satisfies the request. See type definition for more information. - */ - public async getLiquidityForMakerTakerAssetDataPairAsync( - makerAssetData: string, - takerAssetData: string, - ): Promise { - assert.isString('makerAssetData', makerAssetData); - assert.isString('takerAssetData', takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(makerAssetData); - const assetPairs = await this.getAvailableMakerAssetDatasAsync(takerAssetData); - if (!assetPairs.includes(makerAssetData)) { - return { - makerAssetAvailableInBaseUnits: new BigNumber(0), - takerAssetAvailableInBaseUnits: new BigNumber(0), - }; - } - - const ordersWithFillableAmounts = await this.getSignedOrdersWithFillableAmountsAsync( - makerAssetData, - takerAssetData, - ); - return calculateLiquidity(ordersWithFillableAmounts); + return batchSwapQuotes.filter(x => x !== undefined) as MarketBuySwapQuote[]; } /** @@ -428,63 +215,38 @@ export class SwapQuoter { * information for the source. */ public async getBidAskLiquidityForMakerTakerAssetPairAsync( - makerTokenAddress: string, - takerTokenAddress: string, + makerToken: string, + takerToken: string, takerAssetAmount: BigNumber, options: Partial = {}, ): Promise { - assert.isString('makerTokenAddress', makerTokenAddress); - assert.isString('takerTokenAddress', takerTokenAddress); - const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); - const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); + assert.isString('makerToken', makerToken); + assert.isString('takerToken', takerToken); const sourceFilters = new SourceFilters([], options.excludedSources, options.includedSources); + let [sellOrders, buyOrders] = !sourceFilters.isAllowed(ERC20BridgeSource.Native) ? [[], []] : await Promise.all([ - this.orderbook.getOrdersAsync(makerAssetData, takerAssetData), - this.orderbook.getOrdersAsync(takerAssetData, makerAssetData), + this.orderbook.getOrdersAsync(makerToken, takerToken), + this.orderbook.getOrdersAsync(takerToken, makerToken), ]); if (!sellOrders || sellOrders.length === 0) { - sellOrders = [ - { - metaData: {}, - order: createDummyOrderForSampler( - makerAssetData, - takerAssetData, - this._contractAddresses.uniswapBridge, - ), - }, - ]; + sellOrders = [createDummyOrder(makerToken, takerToken)]; } if (!buyOrders || buyOrders.length === 0) { - buyOrders = [ - { - metaData: {}, - order: createDummyOrderForSampler( - takerAssetData, - makerAssetData, - this._contractAddresses.uniswapBridge, - ), - }, - ]; + buyOrders = [createDummyOrder(takerToken, makerToken)]; } + const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => { - const { dexQuotes, nativeOrders, orderFillableAmounts, side } = marketSideLiquidity; + const { dexQuotes, nativeOrders } = marketSideLiquidity.quotes; + const { side } = marketSideLiquidity; + return [ ...dexQuotes, - nativeOrders.map((o, i) => { - // When sell order fillable amount is taker - // When buy order fillable amount is maker - const scaleFactor = orderFillableAmounts[i].div( - side === MarketOperation.Sell ? o.takerAssetAmount : o.makerAssetAmount, - ); + nativeOrders.map(o => { return { - input: (side === MarketOperation.Sell ? o.takerAssetAmount : o.makerAssetAmount) - .times(scaleFactor) - .integerValue(), - output: (side === MarketOperation.Sell ? o.makerAssetAmount : o.takerAssetAmount) - .times(scaleFactor) - .integerValue(), + input: side === MarketOperation.Sell ? o.fillableTakerAmount : o.fillableMakerAmount, + output: side === MarketOperation.Sell ? o.fillableMakerAmount : o.fillableTakerAmount, fillData: o, source: ERC20BridgeSource.Native, }; @@ -492,16 +254,8 @@ export class SwapQuoter { ]; }; const [bids, asks] = await Promise.all([ - this._marketOperationUtils.getMarketBuyLiquidityAsync( - (buyOrders || []).map(o => o.order), - takerAssetAmount, - options, - ), - this._marketOperationUtils.getMarketSellLiquidityAsync( - (sellOrders || []).map(o => o.order), - takerAssetAmount, - options, - ), + this._marketOperationUtils.getMarketBuyLiquidityAsync(buyOrders, takerAssetAmount, options), + this._marketOperationUtils.getMarketSellLiquidityAsync(sellOrders, takerAssetAmount, options), ]); return { bids: getMarketDepthSide(bids), @@ -511,101 +265,6 @@ export class SwapQuoter { }; } - /** - * Get the asset data of all assets that can be used to purchase makerAssetData in the order provider passed in at init. - * - * @return An array of asset data strings that can purchase makerAssetData. - */ - public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise { - assert.isString('makerAssetData', makerAssetData); - assetDataUtils.decodeAssetDataOrThrow(makerAssetData); - const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync(); - const assetPairs = allAssetPairs - .filter(pair => pair.assetDataA.assetData === makerAssetData) - .map(pair => pair.assetDataB.assetData); - return assetPairs; - } - - /** - * Get the asset data of all assets that are purchaseable with takerAssetData in the order provider passed in at init. - * - * @return An array of asset data strings that are purchaseable with takerAssetData. - */ - public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise { - assert.isString('takerAssetData', takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(takerAssetData); - const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync(); - const assetPairs = allAssetPairs - .filter(pair => pair.assetDataB.assetData === takerAssetData) - .map(pair => pair.assetDataA.assetData); - return assetPairs; - } - - /** - * Validates the taker + maker asset pair is available from the order provider provided to `SwapQuote`. - * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * - * @return A boolean on if the taker, maker pair exists - */ - public async isTakerMakerAssetDataPairAvailableAsync( - makerAssetData: string, - takerAssetData: string, - ): Promise { - assert.isString('makerAssetData', makerAssetData); - assert.isString('takerAssetData', takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(makerAssetData); - const availableMakerAssetDatas = await this.getAvailableMakerAssetDatasAsync(takerAssetData); - return _.includes(availableMakerAssetDatas, makerAssetData); - } - - /** - * Grab orders from the order provider, prunes for valid orders with provided OrderPruner options - * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - */ - public async getSignedOrdersWithFillableAmountsAsync( - makerAssetData: string, - takerAssetData: string, - ): Promise { - assert.isString('makerAssetData', makerAssetData); - assert.isString('takerAssetData', takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(makerAssetData); - // get orders - const apiOrders = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData); - const orders = _.map(apiOrders, o => o.order); - const prunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - this.permittedOrderFeeTypes, - this.expiryBufferMs, - ); - const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders); - const ordersWithFillableAmounts = await this._orderStateUtils.getSignedOrdersWithFillableAmountsAsync( - sortedPrunedOrders, - ); - return ordersWithFillableAmounts; - } - - /** - * Util function to check if takerAddress's allowance is enough for 0x exchange contracts to conduct the swap specified by the swapQuote. - * @param swapQuote The swapQuote in question to check enough allowance enabled for 0x exchange contracts to conduct the swap. - * @param takerAddress The address of the taker of the provided swapQuote - */ - public async isSwapQuoteFillableByTakerAddressAsync( - swapQuote: SwapQuote, - takerAddress: string, - ): Promise<[boolean, boolean]> { - const balanceAndAllowance = await this._devUtilsContract - .getBalanceAndAssetProxyAllowance(takerAddress, swapQuote.takerAssetData) - .callAsync(); - return [ - balanceAndAllowance[1].isGreaterThanOrEqualTo(swapQuote.bestCaseQuoteInfo.totalTakerAssetAmount), - balanceAndAllowance[1].isGreaterThanOrEqualTo(swapQuote.worstCaseQuoteInfo.totalTakerAssetAmount), - ]; - } - /** * Returns the recommended gas price for a fast transaction */ @@ -622,46 +281,34 @@ export class SwapQuoter { } /** - * Utility function to get assetData for Ether token. + * Utility function to get Ether token address */ - public async getEtherTokenAssetDataOrThrowAsync(): Promise { - return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken); + public getEtherToken(): string { + return this._contractAddresses.etherToken; } /** - * Grab orders from the order provider, prunes for valid orders with provided OrderPruner options - * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). + * Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address. + * You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption. + * @param makerToken The address of the maker asset + * @param takerToken The address of the taker asset + * @param assetFillAmount If a buy, the amount of maker asset to buy. If a sell, the amount of taker asset to sell. + * @param marketOperation Either a Buy or a Sell quote + * @param options Options for the request. See type definition for more information. + * + * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. */ - private async _getSignedOrdersAsync(makerAssetData: string, takerAssetData: string): Promise { - assert.isString('makerAssetData', makerAssetData); - assert.isString('takerAssetData', takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(takerAssetData); - assetDataUtils.decodeAssetDataOrThrow(makerAssetData); - // get orders - const apiOrders = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData); - const orders = _.map(apiOrders, o => o.order); - const prunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - this.permittedOrderFeeTypes, - this.expiryBufferMs, - ); - return prunedOrders; - } - - /** - * General function for getting swap quote, conditionally uses different logic per specified marketOperation - */ - private async _getSwapQuoteAsync( - makerAssetData: string, - takerAssetData: string, + public async getSwapQuoteAsync( + makerToken: string, + takerToken: string, assetFillAmount: BigNumber, marketOperation: MarketOperation, options: Partial, ): Promise { + assert.isETHAddressHex('makerToken', makerToken); + assert.isETHAddressHex('takerToken', takerToken); + assert.isBigNumber('assetFillAmount', assetFillAmount); const opts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options); - assert.isString('makerAssetData', makerAssetData); - assert.isString('takerAssetData', takerAssetData); let gasPrice: BigNumber; if (!!opts.gasPrice) { gasPrice = opts.gasPrice; @@ -672,112 +319,298 @@ export class SwapQuoter { const sourceFilters = new SourceFilters([], opts.excludedSources, opts.includedSources); + opts.rfqt = this._validateRfqtOpts(sourceFilters, opts.rfqt); + const rfqtOptions = this._rfqtOptions; + + // Get SRA orders (limit orders) + const shouldSkipOpenOrderbook = + !sourceFilters.isAllowed(ERC20BridgeSource.Native) || + (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true); + const nativeOrders = shouldSkipOpenOrderbook + ? await Promise.resolve([]) + : await this.orderbook.getOrdersAsync(makerToken, takerToken, this._limitOrderPruningFn); + + // if no native orders, pass in a dummy order for the sampler to have required metadata for sampling + if (nativeOrders.length === 0) { + nativeOrders.push(createDummyOrder(makerToken, takerToken)); + } + + // ** Prepare options for fetching market side liquidity ** + // Scale fees by gas price. + const cloneOpts = _.omit(opts, 'gasPrice') as GetMarketOrdersOpts; + const calcOpts: GetMarketOrdersOpts = { + ...cloneOpts, + feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData: FillData) => + gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), + ), + exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), + }; + // pass the QuoteRequestor on if rfqt enabled + if (calcOpts.rfqt !== undefined) { + calcOpts.rfqt.quoteRequestor = new QuoteRequestor( + rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {}, + rfqtOptions ? rfqtOptions.warningLogger : undefined, + rfqtOptions ? rfqtOptions.infoLogger : undefined, + this.expiryBufferMs, + ); + } + + const result: OptimizerResultWithReport = await this._marketOperationUtils.getOptimizerResultAsync( + nativeOrders, + assetFillAmount, + marketOperation, + calcOpts, + ); + + const swapQuote = createSwapQuote( + result, + makerToken, + takerToken, + marketOperation, + assetFillAmount, + gasPrice, + opts.gasSchedule, + opts.bridgeSlippage, + ); + + // Use the raw gas, not scaled by gas price + const exchangeProxyOverhead = opts.exchangeProxyOverhead(result.sourceFlags).toNumber(); + swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead; + swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead; + + return swapQuote; + } + + private readonly _limitOrderPruningFn = (limitOrder: SignedNativeOrder) => { + const order = new LimitOrder(limitOrder.order); + const isOpenOrder = order.taker === constants.NULL_ADDRESS; + const willOrderExpire = order.willExpire(this.expiryBufferMs / constants.ONE_SECOND_MS); // tslint:disable-line:boolean-naming + const isFeeTypeAllowed = + this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) && + order.takerTokenFeeAmount.eq(constants.ZERO_AMOUNT); + return isOpenOrder && !willOrderExpire && isFeeTypeAllowed; + }; // tslint:disable-line:semicolon + + private _isApiKeyWhitelisted(apiKey: string | undefined): boolean { + if (!apiKey) { + return false; + } + const whitelistedApiKeys = this._rfqtOptions ? this._rfqtOptions.takerApiKeyWhitelist : []; + return whitelistedApiKeys.includes(apiKey); + } + + private _validateRfqtOpts( + sourceFilters: SourceFilters, + rfqt: RfqtRequestOpts | undefined, + ): RfqtRequestOpts | undefined { + if (!rfqt) { + return rfqt; + } + // tslint:disable-next-line: boolean-naming + const { apiKey, nativeExclusivelyRFQT, intentOnFilling, txOrigin } = rfqt; // If RFQT is enabled and `nativeExclusivelyRFQT` is set, then `ERC20BridgeSource.Native` should // never be excluded. - if ( - opts.rfqt && - opts.rfqt.nativeExclusivelyRFQT === true && - !sourceFilters.isAllowed(ERC20BridgeSource.Native) - ) { + if (nativeExclusivelyRFQT === true && !sourceFilters.isAllowed(ERC20BridgeSource.Native)) { throw new Error('Native liquidity cannot be excluded if "rfqt.nativeExclusivelyRFQT" is set'); } - // get batches of orders from different sources, awaiting sources in parallel - const orderBatchPromises: Array> = []; - - const skipOpenOrderbook = - !sourceFilters.isAllowed(ERC20BridgeSource.Native) || - (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true); - if (!skipOpenOrderbook) { - orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book - } - - const rfqtOptions = this._rfqtOptions; - const quoteRequestor = new QuoteRequestor( - rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {}, - rfqtOptions ? rfqtOptions.warningLogger : undefined, - rfqtOptions ? rfqtOptions.infoLogger : undefined, - this.expiryBufferMs, - ); - // If an API key was provided, but the key is not whitelisted, raise a warning and disable RFQ - if (opts.rfqt && opts.rfqt.apiKey && !this._isApiKeyWhitelisted(opts.rfqt.apiKey)) { - if (rfqtOptions && rfqtOptions.warningLogger) { - rfqtOptions.warningLogger( + if (!this._isApiKeyWhitelisted(apiKey)) { + if (this._rfqtOptions && this._rfqtOptions.warningLogger) { + this._rfqtOptions.warningLogger( { - apiKey: opts.rfqt.apiKey, + apiKey, }, 'Attempt at using an RFQ API key that is not whitelisted. Disabling RFQ for the request lifetime.', ); } - opts.rfqt = undefined; + return undefined; } + // Otherwise check other RFQ options if ( - opts.rfqt && // This is an RFQT-enabled API request - !getPriceAwareRFQRolloutFlags(opts.rfqt.priceAwareRFQFlag).isFirmPriceAwareEnabled && // If Price-aware RFQ is enabled, firm quotes are requested later on in the process. - opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote - opts.rfqt.apiKey && - this._isApiKeyWhitelisted(opts.rfqt.apiKey) && // A valid API key was provided + intentOnFilling && // The requestor is asking for a firm quote + this._isApiKeyWhitelisted(apiKey) && // A valid API key was provided sourceFilters.isAllowed(ERC20BridgeSource.Native) // Native liquidity is not excluded ) { - if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { - throw new Error('RFQ-T requests must specify a taker address'); + if (!txOrigin || txOrigin === constants.NULL_ADDRESS) { + throw new Error('RFQ-T firm quote requests must specify a tx origin'); } - orderBatchPromises.push( - quoteRequestor - .requestRfqtFirmQuotesAsync( - makerAssetData, - takerAssetData, - assetFillAmount, - marketOperation, - undefined, - opts.rfqt, - ) - .then(firmQuotes => firmQuotes.map(quote => quote.signedOrder)), - ); } - const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises); - const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch), []); - const orders = sortingUtils.sortOrders(unsortedOrders); - - // if no native orders, pass in a dummy order for the sampler to have required metadata for sampling - if (orders.length === 0) { - orders.push( - createDummyOrderForSampler(makerAssetData, takerAssetData, this._contractAddresses.uniswapBridge), - ); - } - - let swapQuote: SwapQuote; - - const calcOpts: CalculateSwapQuoteOpts = opts; - - if (calcOpts.rfqt !== undefined) { - calcOpts.rfqt.quoteRequestor = quoteRequestor; - } - - if (marketOperation === MarketOperation.Buy) { - swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - orders, - assetFillAmount, - gasPrice, - calcOpts, - ); - } else { - swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - orders, - assetFillAmount, - gasPrice, - calcOpts, - ); - } - - return swapQuote; - } - private _isApiKeyWhitelisted(apiKey: string): boolean { - const whitelistedApiKeys = this._rfqtOptions ? this._rfqtOptions.takerApiKeyWhitelist : []; - return whitelistedApiKeys.includes(apiKey); + return rfqt; } } // tslint:disable-next-line: max-file-line-count + +// begin formatting and report generation functions +function createSwapQuote( + optimizerResult: OptimizerResultWithReport, + makerToken: string, + takerToken: string, + operation: MarketOperation, + assetFillAmount: BigNumber, + gasPrice: BigNumber, + gasSchedule: FeeSchedule, + slippage: number, +): SwapQuote { + const { optimizedOrders, quoteReport, sourceFlags, takerTokenToEthRate, makerTokenToEthRate } = optimizerResult; + const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]; + + // Calculate quote info + const { bestCaseQuoteInfo, worstCaseQuoteInfo, sourceBreakdown } = isTwoHop + ? calculateTwoHopQuoteInfo(optimizedOrders, operation, gasSchedule, slippage) + : calculateQuoteInfo(optimizedOrders, operation, assetFillAmount, gasPrice, gasSchedule, slippage); + + // Put together the swap quote + const { makerTokenDecimals, takerTokenDecimals } = optimizerResult.marketSideLiquidity; + const swapQuote = { + makerToken, + takerToken, + gasPrice, + orders: optimizedOrders, + bestCaseQuoteInfo, + worstCaseQuoteInfo, + sourceBreakdown, + makerTokenDecimals, + takerTokenDecimals, + takerTokenToEthRate, + makerTokenToEthRate, + quoteReport, + isTwoHop, + }; + + if (operation === MarketOperation.Buy) { + return { + ...swapQuote, + type: MarketOperation.Buy, + makerTokenFillAmount: assetFillAmount, + }; + } else { + return { + ...swapQuote, + type: MarketOperation.Sell, + takerTokenFillAmount: assetFillAmount, + }; + } +} + +function calculateQuoteInfo( + optimizedOrders: OptimizedMarketOrder[], + operation: MarketOperation, + assetFillAmount: BigNumber, + gasPrice: BigNumber, + gasSchedule: FeeSchedule, + slippage: number, +): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { + const bestCaseFillResult = simulateBestCaseFill({ + gasPrice, + orders: optimizedOrders, + side: operation, + fillAmount: assetFillAmount, + opts: { gasSchedule }, + }); + + const worstCaseFillResult = simulateWorstCaseFill({ + gasPrice, + orders: optimizedOrders, + side: operation, + fillAmount: assetFillAmount, + opts: { gasSchedule, slippage }, + }); + + return { + bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), + worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), + sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), + }; +} + +function calculateTwoHopQuoteInfo( + optimizedOrders: OptimizedMarketOrder[], + operation: MarketOperation, + gasSchedule: FeeSchedule, + slippage: number, +): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { + const [firstHopOrder, secondHopOrder] = optimizedOrders; + const [firstHopFill] = firstHopOrder.fills; + const [secondHopFill] = secondHopOrder.fills; + const gas = new BigNumber( + gasSchedule[ERC20BridgeSource.MultiHop]!({ + firstHopSource: _.pick(firstHopFill, 'source', 'fillData'), + secondHopSource: _.pick(secondHopFill, 'source', 'fillData'), + }), + ).toNumber(); + return { + bestCaseQuoteInfo: { + makerAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input, + takerAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output, + totalTakerAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output, + feeTakerTokenAmount: constants.ZERO_AMOUNT, + protocolFeeInWeiAmount: constants.ZERO_AMOUNT, + gas, + }, + // TODO jacob consolidate this with quote simulation worstCase + worstCaseQuoteInfo: { + makerAmount: MarketOperation.Sell + ? secondHopOrder.makerAmount.times(1 - slippage).integerValue() + : secondHopOrder.makerAmount, + takerAmount: MarketOperation.Sell + ? firstHopOrder.takerAmount + : // tslint:disable-next-line: binary-expression-operand-order + firstHopOrder.takerAmount.times(1 + slippage).integerValue(), + totalTakerAmount: firstHopOrder.takerAmount, + feeTakerTokenAmount: constants.ZERO_AMOUNT, + protocolFeeInWeiAmount: constants.ZERO_AMOUNT, + gas, + }, + sourceBreakdown: { + [ERC20BridgeSource.MultiHop]: { + proportion: new BigNumber(1), + intermediateToken: secondHopOrder.takerToken, + hops: [firstHopFill.source, secondHopFill.source], + }, + }, + }; +} + +function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown { + const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); + const breakdown: SwapQuoteOrdersBreakdown = {}; + Object.entries(fillAmountBySource).forEach(([s, fillAmount]) => { + const source = s as keyof SwapQuoteOrdersBreakdown; + if (source === ERC20BridgeSource.MultiHop) { + // TODO jacob has a different breakdown + } else { + breakdown[source] = fillAmount.div(totalFillAmount); + } + }); + return breakdown; +} + +function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo { + return { + makerAmount: fr.totalMakerAssetAmount, + takerAmount: fr.takerAssetAmount, + totalTakerAmount: fr.totalTakerAssetAmount, + feeTakerTokenAmount: fr.takerFeeTakerAssetAmount, + protocolFeeInWeiAmount: fr.protocolFeeAmount, + gas: fr.gas, + }; +} + +function createDummyOrder(makerToken: string, takerToken: string): SignedNativeOrder { + return { + type: FillQuoteTransformerOrderType.Limit, + order: { + ...new LimitOrder({ + makerToken, + takerToken, + makerAmount: ZERO_AMOUNT, + takerAmount: ZERO_AMOUNT, + takerTokenFeeAmount: ZERO_AMOUNT, + }), + }, + signature: INVALID_SIGNATURE, + }; +} diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index e3d48fa259..32f82ad791 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -1,7 +1,13 @@ import { ChainId } from '@0x/contract-addresses'; import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-wrappers'; +import { + FillQuoteTransformerOrderType, + LimitOrderFields, + RfqOrder, + RfqOrderFields, + Signature, +} from '@0x/protocol-utils'; import { TakerRequestQueryParams } from '@0x/quote-server'; -import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { @@ -22,34 +28,23 @@ export interface OrderPrunerOpts { permittedOrderFeeTypes: Set; } -/** - * Represents the on-chain metadata of a signed order - */ -export interface OrderPrunerOnChainMetadata { - orderStatus: number; - orderHash: string; - orderTakerAssetFilledAmount: BigNumber; - fillableTakerAssetAmount: BigNumber; - isValidSignature: boolean; +export interface SignedOrder { + order: T; + type: FillQuoteTransformerOrderType.Limit | FillQuoteTransformerOrderType.Rfq; + signature: Signature; } -/** - * makerAssetData: The assetData representing the desired makerAsset. - * takerAssetData: The assetData representing the desired takerAsset. - */ -export interface OrderProviderRequest { - makerAssetData: string; - takerAssetData: string; -} +export type SignedNativeOrder = SignedOrder | SignedOrder; +export type NativeOrderWithFillableAmounts = SignedNativeOrder & NativeOrderFillableAmountFields; /** - * fillableMakerAssetAmount: Amount of makerAsset that is fillable - * fillableTakerAssetAmount: Amount of takerAsset that is fillable - * fillableTakerFeeAmount: Amount of takerFee paid to fill fillableTakerAssetAmount + * fillableMakerAmount: Amount of makerAsset that is fillable + * fillableTakerAmount: Amount of takerAsset that is fillable + * fillableTakerFeeAmount: Amount of takerFee paid to fill fillableTakerAmount */ -export interface SignedOrderWithFillableAmounts extends SignedOrder { - fillableMakerAssetAmount: BigNumber; - fillableTakerAssetAmount: BigNumber; +export interface NativeOrderFillableAmountFields { + fillableMakerAmount: BigNumber; + fillableTakerAmount: BigNumber; fillableTakerFeeAmount: BigNumber; } @@ -67,24 +62,6 @@ export interface CalldataInfo { allowanceTarget: string; } -/** - * Represents the varying smart contracts that can consume a valid swap quote - */ -export enum ExtensionContractType { - None = 'NONE', - Forwarder = 'FORWARDER', - ExchangeProxy = 'EXCHANGE_PROXY', -} - -/** - * feePercentage: Optional affiliate fee percentage used to calculate the eth amount paid to fee recipient. - * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000). - */ -export interface ForwarderSmartContractParamsBase { - feePercentage: BigNumber; - feeRecipient: string; -} - /** * Interface that varying SwapQuoteConsumers adhere to (exchange consumer, router consumer, forwarder consumer, coordinator consumer) * getCalldataOrThrow: Get CalldataInfo to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided. @@ -107,8 +84,7 @@ export interface SwapQuoteConsumerOpts { * Represents the options provided to a generic SwapQuoteConsumer */ export interface SwapQuoteGetOutputOpts { - useExtensionContract: ExtensionContractType; - extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any; + extensionContractOpts?: ExchangeProxyContractOpts | any; } /** @@ -122,15 +98,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts { gasLimit?: number; } -/** - * feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient - * feeRecipient: address of the receiver of the feePercentage of taker asset - */ -export interface ForwarderExtensionContractOpts { - feePercentage: number; - feeRecipient: string; -} - export interface AffiliateFee { recipient: string; buyTokenFeeAmount: BigNumber; @@ -175,31 +142,27 @@ export interface GetExtensionContractTypeOpts { } /** - * takerAssetData: String that represents a specific taker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * makerAssetData: String that represents a specific maker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). + * takerToken: Address of the taker asset. + * makerToken: Address of the maker asset. * gasPrice: gas price used to determine protocolFee amount, default to ethGasStation fast amount. * orders: An array of objects conforming to OptimizedMarketOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. * bestCaseQuoteInfo: Info about the best case price for the asset. * worstCaseQuoteInfo: Info about the worst case price for the asset. - * unoptimizedQuoteInfo: Info about the unoptimized (best single source) price for the swap - * unoptimizedOrders: Orders used in the unoptimized quote info */ export interface SwapQuoteBase { - takerAssetData: string; - makerAssetData: string; + takerToken: string; + makerToken: string; gasPrice: BigNumber; orders: OptimizedMarketOrder[]; bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown; quoteReport?: QuoteReport; - unoptimizedQuoteInfo: SwapQuoteInfo; - unoptimizedOrders: OptimizedMarketOrder[]; isTwoHop: boolean; makerTokenDecimals: number; takerTokenDecimals: number; - takerAssetToEthRate: BigNumber; - makerAssetToEthRate: BigNumber; + takerTokenToEthRate: BigNumber; + makerTokenToEthRate: BigNumber; } /** @@ -207,7 +170,7 @@ export interface SwapQuoteBase { * type: Specified MarketOperation the SwapQuote is provided for */ export interface MarketSellSwapQuote extends SwapQuoteBase { - takerAssetFillAmount: BigNumber; + takerTokenFillAmount: BigNumber; type: MarketOperation.Sell; } @@ -216,25 +179,25 @@ export interface MarketSellSwapQuote extends SwapQuoteBase { * type: Specified MarketOperation the SwapQuote is provided for */ export interface MarketBuySwapQuote extends SwapQuoteBase { - makerAssetFillAmount: BigNumber; + makerTokenFillAmount: BigNumber; type: MarketOperation.Buy; } export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote; /** - * feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets. - * takerAssetAmount: The amount of takerAsset swapped for desired makerAsset. - * totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees). - * makerAssetAmount: The amount of makerAsset that will be acquired through the swap. + * feeTakerTokenAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets. + * takerTokenAmount: The amount of takerAsset swapped for desired makerAsset. + * totalTakerTokenAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees). + * makerTokenAmount: The amount of makerAsset that will be acquired through the swap. * protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset. * gas: Amount of estimated gas needed to fill the quote. */ export interface SwapQuoteInfo { - feeTakerAssetAmount: BigNumber; - takerAssetAmount: BigNumber; - totalTakerAssetAmount: BigNumber; - makerAssetAmount: BigNumber; + feeTakerTokenAmount: BigNumber; + takerAmount: BigNumber; + totalTakerAmount: BigNumber; + makerAmount: BigNumber; protocolFeeInWeiAmount: BigNumber; gas: number; } @@ -252,11 +215,6 @@ export type SwapQuoteOrdersBreakdown = Partial< } >; -export interface PriceAwareRFQFlags { - isIndicativePriceAwareEnabled: boolean; - isFirmPriceAwareEnabled: boolean; -} - /** * nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity. * If set to `true` and `ERC20BridgeSource.Native` is part of the `excludedSources` @@ -264,37 +222,22 @@ export interface PriceAwareRFQFlags { */ export interface RfqtRequestOpts { takerAddress: string; + txOrigin: string; apiKey: string; intentOnFilling: boolean; isIndicative?: boolean; makerEndpointMaxResponseTimeMs?: number; nativeExclusivelyRFQT?: boolean; - - /** - * This feature flag allows us to merge the price-aware RFQ pricing - * project while still controlling when to activate the feature. We plan to do some - * data analysis work and address some of the issues with maker fillable amounts - * in later milestones. Once the feature is fully rolled out and is providing value - * and we have assessed that there is no user impact, we will proceed in cleaning up - * the feature flag. When that time comes, follow this PR to "undo" the feature flag: - * https://github.com/0xProject/0x-monorepo/pull/2735 - */ - priceAwareRFQFlag?: PriceAwareRFQFlags; } /** * gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount */ -export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts { +export interface SwapQuoteRequestOpts extends GetMarketOrdersOpts { gasPrice?: BigNumber; rfqt?: RfqtRequestOpts; } -/** - * Opts required to generate a SwapQuote with SwapQuoteCalculator - */ -export interface CalculateSwapQuoteOpts extends GetMarketOrdersOpts {} - /** * A mapping from RFQ-T quote provider URLs to the trading pairs they support. * The value type represents an array of supported asset pairs, with each array element encoded as a 2-element array of token addresses. @@ -306,7 +249,7 @@ export interface RfqtMakerAssetOfferings { export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; export interface RfqtFirmQuoteValidator { - getRfqtTakerFillableAmountsAsync(quotes: SignedOrder[]): Promise; + getRfqtTakerFillableAmountsAsync(quotes: RfqOrder[]): Promise; } export interface SwapQuoterRfqtOpts { @@ -363,14 +306,6 @@ export enum SwapQuoterError { AssetDataUnsupported = 'ASSET_DATA_UNSUPPORTED', } -/** - * Represents available liquidity for a given assetData. - */ -export interface LiquidityForTakerMakerAssetDataPair { - makerAssetAvailableInBaseUnits: BigNumber; - takerAssetAvailableInBaseUnits: BigNumber; -} - /** * Represents two main market operations supported by asset-swapper. */ @@ -384,25 +319,13 @@ export enum MarketOperation { */ export enum OrderPrunerPermittedFeeTypes { NoFees = 'NO_FEES', - MakerDenominatedTakerFee = 'MAKER_DENOMINATED_TAKER_FEE', TakerDenominatedTakerFee = 'TAKER_DENOMINATED_TAKER_FEE', } /** * Represents a mocked RFQT maker responses. */ -export interface MockedRfqtFirmQuoteResponse { - endpoint: string; - requestApiKey: string; - requestParams: TakerRequestQueryParams; - responseData: any; - responseCode: number; -} - -/** - * Represents a mocked RFQT maker responses. - */ -export interface MockedRfqtIndicativeQuoteResponse { +export interface MockedRfqtQuoteResponse { endpoint: string; requestApiKey: string; requestParams: TakerRequestQueryParams; diff --git a/packages/asset-swapper/src/utils/affiliate_fee_utils.ts b/packages/asset-swapper/src/utils/affiliate_fee_utils.ts index 4ae46a6d33..2f4bc93123 100644 --- a/packages/asset-swapper/src/utils/affiliate_fee_utils.ts +++ b/packages/asset-swapper/src/utils/affiliate_fee_utils.ts @@ -11,7 +11,7 @@ export const affiliateFeeUtils = { * @param feePercentage Percentage of additive fees to apply to totalTakerAssetAmount + protocol fee. */ getTotalEthAmountWithAffiliateFee(swapQuoteInfo: SwapQuoteInfo, feePercentage: number): BigNumber { - const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAssetAmount); + const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAmount); const ethAmountWithFees = ethAmount.plus(affiliateFeeUtils.getFeeAmount(swapQuoteInfo, feePercentage)); return ethAmountWithFees; }, @@ -22,7 +22,7 @@ export const affiliateFeeUtils = { */ getFeeAmount(swapQuoteInfo: SwapQuoteInfo, feePercentage: number): BigNumber { assert.assert(feePercentage >= 0, 'feePercentage must be >= 0'); - const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAssetAmount); + const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAmount); // HACK(dekz): This is actually in WEI amount not ETH return ethAmount.times(feePercentage).integerValue(BigNumber.ROUND_UP); }, diff --git a/packages/asset-swapper/src/utils/assert.ts b/packages/asset-swapper/src/utils/assert.ts index 04709c398c..7392a4fc1c 100644 --- a/packages/asset-swapper/src/utils/assert.ts +++ b/packages/asset-swapper/src/utils/assert.ts @@ -1,130 +1,16 @@ import { assert as sharedAssert } from '@0x/assert'; -import { schemas } from '@0x/json-schemas'; -import { Orderbook } from '@0x/orderbook'; -import { Order, SignedOrder } from '@0x/types'; import * as _ from 'lodash'; -import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types'; - -import { - isAssetDataEquivalent, - isExactAssetData, - isOrderTakerFeePayableWithMakerAsset, - isOrderTakerFeePayableWithTakerAsset, -} from './utils'; +import { Orderbook } from '../swap_quoter'; export const assert = { ...sharedAssert, - isValidSwapQuote(variableName: string, swapQuote: SwapQuote): void { - sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData); - sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData); - sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema); - if (swapQuote.isTwoHop) { - assert.isValidTwoHopSwapQuoteOrders( - `${variableName}.orders`, - swapQuote.orders, - swapQuote.makerAssetData, - swapQuote.takerAssetData, - ); - } else { - assert.isValidSwapQuoteOrders( - `${variableName}.orders`, - swapQuote.orders, - swapQuote.makerAssetData, - swapQuote.takerAssetData, - ); - } - assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo); - assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo); - if (swapQuote.type === MarketOperation.Buy) { - sharedAssert.isBigNumber(`${variableName}.makerAssetFillAmount`, swapQuote.makerAssetFillAmount); - } else { - sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount); - } - }, - isValidSwapQuoteOrders( - variableName: string, - orders: SignedOrder[], - makerAssetData: string, - takerAssetData: string, - ): void { - _.every(orders, (order: SignedOrder, index: number) => { - assert.assert( - isAssetDataEquivalent(takerAssetData, order.takerAssetData), - `Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData} but found ${ - order.takerAssetData - }`, - ); - assert.assert( - isAssetDataEquivalent(makerAssetData, order.makerAssetData), - `Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData} but found ${ - order.makerAssetData - }`, - ); - }); - }, - isValidTwoHopSwapQuoteOrders( - variableName: string, - orders: SignedOrder[], - makerAssetData: string, - takerAssetData: string, - ): void { - assert.assert(orders.length === 2, `Expected ${variableName}.length to be 2 for a two-hop quote`); - assert.assert( - isAssetDataEquivalent(takerAssetData, orders[0].takerAssetData), - `Expected ${variableName}[0].takerAssetData to be ${takerAssetData} but found ${orders[0].takerAssetData}`, - ); - assert.assert( - isAssetDataEquivalent(makerAssetData, orders[1].makerAssetData), - `Expected ${variableName}[1].makerAssetData to be ${makerAssetData} but found ${orders[1].makerAssetData}`, - ); - assert.assert( - isAssetDataEquivalent(orders[0].makerAssetData, orders[1].takerAssetData), - `Expected ${variableName}[0].makerAssetData (${ - orders[0].makerAssetData - }) to equal ${variableName}[1].takerAssetData (${orders[1].takerAssetData})`, - ); - }, - isValidOrdersForSwapQuoter(variableName: string, orders: T[]): void { - _.every(orders, (order: T, index: number) => { - assert.assert( - order.takerFee.isZero() || - isOrderTakerFeePayableWithTakerAsset(order) || - isOrderTakerFeePayableWithMakerAsset(order), - `Expected ${variableName}[${index}].takerFeeAssetData to be ${order.makerAssetData} or ${ - order.takerAssetData - } but found ${order.takerFeeAssetData}`, - ); - }); - }, - isValidForwarderSwapQuote(variableName: string, swapQuote: SwapQuote, wethAssetData: string): void { - assert.isValidSwapQuote(variableName, swapQuote); - assert.isValidForwarderSignedOrders(`${variableName}.orders`, swapQuote.orders, wethAssetData); - }, - isValidForwarderSignedOrders(variableName: string, orders: SignedOrder[], wethAssetData: string): void { - _.forEach(orders, (o: SignedOrder, i: number) => { - assert.isValidForwarderSignedOrder(`${variableName}[${i}]`, o, wethAssetData); - }); - }, - isValidForwarderSignedOrder(variableName: string, order: SignedOrder, wethAssetData: string): void { - assert.assert( - isExactAssetData(order.takerAssetData, wethAssetData), - `Expected ${variableName} to have takerAssetData set as ${wethAssetData}, but is ${order.takerAssetData}`, - ); - }, - isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void { - sharedAssert.isNumber(`${variableName}.gas`, swapQuoteInfo.gas); - sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount); - sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount); - sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount); - sharedAssert.isBigNumber(`${variableName}.makerAssetAmount`, swapQuoteInfo.makerAssetAmount); - }, isValidOrderbook(variableName: string, orderFetcher: Orderbook): void { - sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync); - }, - isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void { - sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); - sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData); + sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync.bind(orderFetcher)); + sharedAssert.isFunction( + `${variableName}.getBatchOrdersAsync`, + orderFetcher.getBatchOrdersAsync.bind(orderFetcher), + ); }, isValidPercentage(variableName: string, percentage: number): void { assert.isNumber(variableName, percentage); @@ -133,8 +19,4 @@ export const assert = { `Expected ${variableName} to be between 0 and 1, but is ${percentage}`, ); }, - isValidForwarderExtensionContractOpts(variableName: string, opts: any): void { - assert.isValidPercentage(`${variableName}.feePercentage`, opts.feePercentage); - assert.isETHAddressHex(`${variableName}.feeRecipient`, opts.feeRecipient); - }, }; diff --git a/packages/asset-swapper/src/utils/calculate_liquidity.ts b/packages/asset-swapper/src/utils/calculate_liquidity.ts deleted file mode 100644 index 4a0711d55f..0000000000 --- a/packages/asset-swapper/src/utils/calculate_liquidity.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BigNumber } from '@0x/utils'; - -import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../types'; - -import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; - -export const calculateLiquidity = ( - prunedOrders: SignedOrderWithFillableAmounts[], -): LiquidityForTakerMakerAssetDataPair => { - const liquidityInBigNumbers = prunedOrders.reduce( - (acc, order) => { - const fillableMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order) - ? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount) - : order.fillableMakerAssetAmount; - const fillableTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order) - ? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount) - : order.fillableTakerAssetAmount; - return { - makerAssetAvailableInBaseUnits: acc.makerAssetAvailableInBaseUnits.plus(fillableMakerAssetAmount), - takerAssetAvailableInBaseUnits: acc.takerAssetAvailableInBaseUnits.plus(fillableTakerAssetAmount), - }; - }, - { - makerAssetAvailableInBaseUnits: new BigNumber(0), - takerAssetAvailableInBaseUnits: new BigNumber(0), - }, - ); - return liquidityInBigNumbers; -}; diff --git a/packages/asset-swapper/src/utils/fillable_amounts_utils.ts b/packages/asset-swapper/src/utils/fillable_amounts_utils.ts deleted file mode 100644 index 9f2e8308e6..0000000000 --- a/packages/asset-swapper/src/utils/fillable_amounts_utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; - -import { SignedOrderWithFillableAmounts } from '../types'; - -import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; - -export const fillableAmountsUtils = { - getTakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber { - if (isOrderTakerFeePayableWithTakerAsset(order)) { - return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount); - } else { - return order.fillableTakerAssetAmount; - } - }, - getMakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber { - if (isOrderTakerFeePayableWithMakerAsset(order)) { - return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount); - } else { - return order.fillableMakerAssetAmount; - } - }, -}; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts index 6a76c29f03..9e22ea2d30 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts @@ -1,48 +1,47 @@ -import { BigNumber } from '@0x/utils'; -import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; -import { Decimal } from 'decimal.js'; +import { getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; +import { Pool } from '@balancer-labs/sor/dist/types'; -import { ZERO_AMOUNT } from './constants'; +import { BALANCER_MAX_POOLS_FETCHED, BALANCER_SUBGRAPH_URL, BALANCER_TOP_POOLS_FETCHED } from './constants'; // tslint:disable:boolean-naming -export interface BalancerPool { - id: string; - balanceIn: BigNumber; - balanceOut: BigNumber; - weightIn: BigNumber; - weightOut: BigNumber; - swapFee: BigNumber; - spotPrice?: BigNumber; - slippage?: BigNumber; - limitAmount?: BigNumber; -} - interface CacheValue { timestamp: number; - pools: BalancerPool[]; + pools: Pool[]; } // tslint:disable:custom-no-magic-numbers const FIVE_SECONDS_MS = 5 * 1000; const ONE_DAY_MS = 24 * 60 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 1000; -const MAX_POOLS_FETCHED = 3; -const Decimal20 = Decimal.clone({ precision: 20 }); // tslint:enable:custom-no-magic-numbers +interface BalancerPoolResponse { + id: string; + swapFee: string; + tokens: Array<{ address: string; decimals: number; balance: string }>; + tokensList: string[]; + totalWeight: string; +} + export class BalancerPoolsCache { constructor( private readonly _cache: { [key: string]: CacheValue } = {}, - private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED, - ) {} + private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, + private readonly subgraphUrl: string = BALANCER_SUBGRAPH_URL, + private readonly topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED, + ) { + void this._loadTopPoolsAsync(); + // Reload the top pools every 12 hours + setInterval(async () => void this._loadTopPoolsAsync(), ONE_DAY_MS / 2); + } public async getPoolsForPairAsync( takerToken: string, makerToken: string, timeoutMs: number = DEFAULT_TIMEOUT_MS, - ): Promise { - const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); + ): Promise { + const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); } @@ -88,23 +87,26 @@ export class BalancerPoolsCache { takerToken: string, makerToken: string, cacheExpiryMs: number = FIVE_SECONDS_MS, - ): Promise { + ): Promise { const key = JSON.stringify([takerToken, makerToken]); const value = this._cache[key]; const minTimestamp = Date.now() - cacheExpiryMs; if (value === undefined || value.timestamp < minTimestamp) { const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); - const timestamp = Date.now(); - this._cache[key] = { - pools, - timestamp, - }; + this._cachePoolsForPair(takerToken, makerToken, pools); } return this._cache[key].pools; } - // tslint:disable-next-line:prefer-function-over-method - protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[]): void { + const key = JSON.stringify([takerToken, makerToken]); + this._cache[key] = { + pools, + timestamp: Date.now(), + }; + } + + protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { try { const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; // Sort by maker token balance (descending) @@ -116,36 +118,75 @@ export class BalancerPoolsCache { return []; } } -} -// tslint:disable completed-docs -export function computeBalancerSellQuote(pool: BalancerPool, takerFillAmount: BigNumber): BigNumber { - if (takerFillAmount.isGreaterThan(bmath.bmul(pool.balanceIn, bmath.MAX_IN_RATIO))) { - return ZERO_AMOUNT; - } - const weightRatio = pool.weightIn.dividedBy(pool.weightOut); - const adjustedIn = bmath.BONE.minus(pool.swapFee) - .dividedBy(bmath.BONE) - .times(takerFillAmount); - const y = pool.balanceIn.dividedBy(pool.balanceIn.plus(adjustedIn)); - const foo = Math.pow(y.toNumber(), weightRatio.toNumber()); - const bar = new BigNumber(1).minus(foo); - const tokenAmountOut = pool.balanceOut.times(bar); - return tokenAmountOut.integerValue(); -} + protected async _loadTopPoolsAsync(): Promise { + const fromToPools: { + [from: string]: { [to: string]: Pool[] }; + } = {}; -export function computeBalancerBuyQuote(pool: BalancerPool, makerFillAmount: BigNumber): BigNumber { - if (makerFillAmount.isGreaterThan(bmath.bmul(pool.balanceOut, bmath.MAX_OUT_RATIO))) { - return ZERO_AMOUNT; + const pools = await this._fetchTopPoolsAsync(); + pools.forEach(pool => { + const { tokensList } = pool; + for (const from of tokensList) { + for (const to of tokensList.filter(t => t.toLowerCase() !== from.toLowerCase())) { + if (!fromToPools[from]) { + fromToPools[from] = {}; + } + if (!fromToPools[from][to]) { + fromToPools[from][to] = []; + } + try { + // The list of pools must be relevant to `from` and `to` for `parsePoolData` + const poolData = parsePoolData([pool], from, to); + fromToPools[from][to].push(poolData[0]); + // Cache this as we progress through + this._cachePoolsForPair(from, to, fromToPools[from][to]); + } catch { + // soldier on + } + } + } + }); } - const weightRatio = pool.weightOut.dividedBy(pool.weightIn); - const diff = pool.balanceOut.minus(makerFillAmount); - const y = pool.balanceOut.dividedBy(diff); - let foo: number | Decimal = Math.pow(y.toNumber(), weightRatio.toNumber()) - 1; - if (!Number.isFinite(foo)) { - foo = new Decimal20(y.toString()).pow(weightRatio.toString()).minus(1); + + protected async _fetchTopPoolsAsync(): Promise { + const query = ` + query { + pools (first: ${ + this.topPoolsFetched + }, where: {publicSwap: true, liquidity_gt: 0}, orderBy: swapsCount, orderDirection: desc) { + id + publicSwap + swapFee + totalWeight + tokensList + tokens { + id + address + balance + decimals + symbol + denormWeight + } + } + } + `; + try { + const response = await fetch(this.subgraphUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + }), + }); + + const { data } = await response.json(); + return data.pools; + } catch (err) { + return []; + } } - let tokenAmountIn = bmath.BONE.minus(pool.swapFee).dividedBy(bmath.BONE); - tokenAmountIn = pool.balanceIn.times(foo.toString()).dividedBy(tokenAmountIn); - return tokenAmountIn.integerValue(); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts b/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts index 965d95221e..231440bc51 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts @@ -1,4 +1,5 @@ import { Web3Wrapper } from '@0x/dev-utils'; +import { FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { BigNumber, logUtils } from '@0x/utils'; import * as _ from 'lodash'; @@ -38,7 +39,9 @@ export function getComparisonPrices( return { wholeOrder }; } else { try { - feeInEth = new BigNumber((feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)(undefined)); + feeInEth = new BigNumber( + (feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)({ type: FillQuoteTransformerOrderType.Rfq }), + ); } catch { logUtils.warn('Native order fee schedule requires fill data'); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 969cd2314d..0124cc44da 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -405,16 +405,23 @@ export const MAINNET_SNOWSWAP_INFOS: { [name: string]: CurveInfo } = { */ export const KYBER_BRIDGED_LIQUIDITY_PREFIX = '0xbb'; export const MAX_KYBER_RESERVES_QUERIED = 5; +export const MAINNET_KYBER_NETWORK_PROXY = '0x9aab3f75489902f3a48495025729a0af77d4b11e'; export const LIQUIDITY_PROVIDER_REGISTRY: LiquidityProviderRegistry = {}; -export const MAINNET_SUSHI_SWAP_ROUTER = '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'; -export const MAINNET_CRYPTO_COM_ROUTER = '0xCeB90E4C17d626BE0fACd78b79c9c87d7ca181b3'; +export const MAINNET_UNISWAP_V1_ROUTER = '0xc0a47dfe034b400b47bdad5fecda2621de6c4d95'; +export const MAINNET_UNISWAP_V2_ROUTER = '0xf164fc0ec4e93095b804a4795bbe1e041497b92a'; +export const MAINNET_SUSHI_SWAP_ROUTER = '0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f'; +export const MAINNET_CRYPTO_COM_ROUTER = '0xceb90e4c17d626be0facd78b79c9c87d7ca181b3'; +export const MAINNET_MSTABLE_ROUTER = '0xe2f2a5c287993345a840db3b0845fbc70f5935a5'; +export const MAINNET_OASIS_ROUTER = '0x794e6e91555438afc3ccf1c5076a74f42133d08d'; export const MAINNET_MOONISWAP_REGISTRY = '0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303'; export const MAINNET_MOONISWAP_V2_REGISTRY = '0xc4a8b7e29e3c8ec560cd4945c1cf3461a85a148d'; export const MAINNET_MOONISWAP_V2_1_REGISTRY = '0xbaf9a5d4b0052359326a6cdab54babaa3a3a9643'; +export const MAINNET_DODO_HELPER = '0x533da777aedce766ceae696bf90f8541a4ba80eb'; + export const MAINNET_SHELL_POOLS = { StableCoins: { poolAddress: '0x8f26d7bab7a73309141a291525c965ecdea7bf42', @@ -426,6 +433,10 @@ export const MAINNET_SHELL_POOLS = { }, }; +export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'; +export const BALANCER_TOP_POOLS_FETCHED = 250; +export const BALANCER_MAX_POOLS_FETCHED = 3; + export const ERC20_PROXY_ID = '0xf47261b0'; export const WALLET_SIGNATURE = '0x04'; export const ONE_ETHER = new BigNumber(1e18); @@ -502,7 +513,10 @@ export const BRIDGE_ADDRESSES_BY_CHAIN: { [chainId in ChainId]: BridgeContractAd */ // tslint:disable:custom-no-magic-numbers export const DEFAULT_GAS_SCHEDULE: Required = { - [ERC20BridgeSource.Native]: () => 150e3, + [ERC20BridgeSource.Native]: _fillData => { + // const nativeFillData = (_fillData as NativeRfqOrderFillData|NativeLimitOrderFillData) + return 100e3; + }, [ERC20BridgeSource.Uniswap]: () => 90e3, [ERC20BridgeSource.LiquidityProvider]: fillData => { return (fillData as LiquidityProviderFillData).gasCost; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts index a31e420b22..656d2a96c3 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts @@ -1,33 +1,33 @@ +import { Pool } from '@balancer-labs/sor/dist/types'; import { getPoolsWithTokens, parsePoolData } from 'cream-sor'; -import { BalancerPool } from './balancer_utils'; +import { BALANCER_MAX_POOLS_FETCHED } from './constants'; // tslint:disable:boolean-naming interface CacheValue { timestamp: number; - pools: BalancerPool[]; + pools: Pool[]; } // tslint:disable:custom-no-magic-numbers const FIVE_SECONDS_MS = 5 * 1000; const ONE_DAY_MS = 24 * 60 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 1000; -const MAX_POOLS_FETCHED = 3; // tslint:enable:custom-no-magic-numbers export class CreamPoolsCache { constructor( private readonly _cache: { [key: string]: CacheValue } = {}, - private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED, + private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, ) {} public async getPoolsForPairAsync( takerToken: string, makerToken: string, timeoutMs: number = DEFAULT_TIMEOUT_MS, - ): Promise { - const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); + ): Promise { + const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); } @@ -73,7 +73,7 @@ export class CreamPoolsCache { takerToken: string, makerToken: string, cacheExpiryMs: number = FIVE_SECONDS_MS, - ): Promise { + ): Promise { const key = JSON.stringify([takerToken, makerToken]); const value = this._cache[key]; const minTimestamp = Date.now() - cacheExpiryMs; @@ -88,8 +88,13 @@ export class CreamPoolsCache { return this._cache[key].pools; } + // tslint:disable-next-line: prefer-function-over-method + protected async _loadTopPoolsAsync(): Promise { + // Do nothing + } + // tslint:disable-next-line:prefer-function-over-method - protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { try { const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; // Sort by maker token balance (descending) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 4806c71c09..74a4515b9a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -1,7 +1,7 @@ +import { FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; -import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; -import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; +import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types'; import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types'; @@ -13,7 +13,7 @@ import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types'; */ export function createFills(opts: { side: MarketOperation; - orders?: SignedOrderWithFillableAmounts[]; + orders?: NativeOrderWithFillableAmounts[]; dexQuotes?: DexSample[][]; targetInput?: BigNumber; ethToOutputRate?: BigNumber; @@ -31,7 +31,7 @@ export function createFills(opts: { // Create native fills. const nativeFills = nativeOrdersToFills( side, - orders, + orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)), opts.targetInput, ethToOutputRate, ethToInputRate, @@ -73,7 +73,7 @@ function hasLiquidity(fills: Fill[]): boolean { function nativeOrdersToFills( side: MarketOperation, - orders: SignedOrderWithFillableAmounts[], + orders: NativeOrderWithFillableAmounts[], targetInput: BigNumber = POSITIVE_INF, ethToOutputRate: BigNumber, ethToInputRate: BigNumber, @@ -82,12 +82,13 @@ function nativeOrdersToFills( const sourcePathId = hexUtils.random(); // Create a single path from all orders. let fills: Array = []; - for (const order of orders) { - const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order); - const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); + for (const o of orders) { + const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount } = o; + const makerAmount = fillableMakerAmount; + const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount); const input = side === MarketOperation.Sell ? takerAmount : makerAmount; const output = side === MarketOperation.Sell ? makerAmount : takerAmount; - const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(); + const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); const outputPenalty = !ethToOutputRate.isZero() ? ethToOutputRate.times(fee) : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); @@ -117,7 +118,8 @@ function nativeOrdersToFills( index: 0, // TBD parent: undefined, // TBD source: ERC20BridgeSource.Native, - fillData: { order }, + type: o.type, + fillData: { ...o }, }); } // Sort by descending adjusted rate. @@ -167,6 +169,7 @@ function dexSamplesToFills( adjustedOutput, source, fillData, + type: FillQuoteTransformerOrderType.Bridge, index: i, parent: i !== 0 ? fills[fills.length - 1] : undefined, flags: SOURCE_FLAGS[source], diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 338319eb29..637ac1a616 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -1,12 +1,20 @@ -import { V3RFQIndicativeQuote } from '@0x/quote-server'; -import { SignedOrder } from '@0x/types'; +import { FillQuoteTransformerOrderType, RfqOrder } from '@0x/protocol-utils'; import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import * as _ from 'lodash'; -import { DEFAULT_INFO_LOGGER } from '../../constants'; -import { AssetSwapperContractAddresses, MarketOperation } from '../../types'; +import { DEFAULT_INFO_LOGGER, INVALID_SIGNATURE } from '../../constants'; +import { + AssetSwapperContractAddresses, + MarketOperation, + NativeOrderWithFillableAmounts, + SignedNativeOrder, +} from '../../types'; import { QuoteRequestor } from '../quote_requestor'; -import { getPriceAwareRFQRolloutFlags } from '../utils'; +import { + getNativeAdjustedFillableAmountsFromMakerAmount, + getNativeAdjustedFillableAmountsFromTakerAmount, + getNativeAdjustedMakerFillAmount, +} from '../utils'; import { generateQuoteReport, QuoteReport } from './../quote_report_generator'; import { getComparisonPrices } from './comparison_price'; @@ -21,13 +29,8 @@ import { } from './constants'; import { createFills } from './fills'; import { getBestTwoHopQuote } from './multihop_utils'; -import { - createOrdersFromTwoHopSample, - createSignedOrdersFromRfqtIndicativeQuotes, - createSignedOrdersWithFillableAmounts, - getNativeOrderTokens, -} from './orders'; -import { fillsToSortedPaths, findOptimalPathAsync } from './path_optimizer'; +import { createOrdersFromTwoHopSample } from './orders'; +import { findOptimalPathAsync } from './path_optimizer'; import { DexOrderSampler, getSampleAmounts } from './sampler'; import { SourceFilters } from './source_filters'; import { @@ -45,36 +48,6 @@ import { // tslint:disable:boolean-naming -/** - * Returns a indicative quotes or an empty array if RFQT is not enabled or requested - * @param makerAssetData the maker asset data - * @param takerAssetData the taker asset data - * @param marketOperation Buy or Sell - * @param assetFillAmount the amount to fill, in base units - * @param opts market request options - */ -export async function getRfqtIndicativeQuotesAsync( - makerAssetData: string, - takerAssetData: string, - marketOperation: MarketOperation, - assetFillAmount: BigNumber, - comparisonPrice: BigNumber | undefined, - opts: Partial, -): Promise { - if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) { - return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( - makerAssetData, - takerAssetData, - assetFillAmount, - marketOperation, - comparisonPrice, - opts.rfqt, - ); - } else { - return Promise.resolve([]); - } -} - export class MarketOperationUtils { private readonly _wethAddress: string; private readonly _sellSources: SourceFilters; @@ -87,14 +60,14 @@ export class MarketOperationUtils { optimizerResult: OptimizerResult, comparisonPrice?: BigNumber | undefined, ): QuoteReport { - const { side, dexQuotes, twoHopQuotes, orderFillableAmounts, nativeOrders } = marketSideLiquidity; + const { side, quotes } = marketSideLiquidity; + const { dexQuotes, twoHopQuotes, nativeOrders } = quotes; const { liquidityDelivered } = optimizerResult; return generateQuoteReport( side, _.flatten(dexQuotes), twoHopQuotes, nativeOrders, - orderFillableAmounts, liquidityDelivered, comparisonPrice, quoteRequestor, @@ -113,26 +86,22 @@ export class MarketOperationUtils { /** * Gets the liquidity available for a market sell operation - * @param nativeOrders Native orders. + * @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders * @param takerAmount Amount of taker asset to sell. * @param opts Options object. * @return MarketSideLiquidity. */ public async getMarketSellLiquidityAsync( - nativeOrders: SignedOrder[], + nativeOrders: SignedNativeOrder[], takerAmount: BigNumber, opts?: Partial, ): Promise { - if (nativeOrders.length === 0) { - throw new Error(AggregationError.EmptyOrders); - } const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); + const { makerToken, takerToken } = nativeOrders[0].order; const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase); const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources); const quoteSourceFilters = this._sellSources.merge(requestFilters); - const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); const { @@ -158,11 +127,14 @@ export class MarketOperationUtils { ...(!sampleBalancerOnChain ? [ERC20BridgeSource.Balancer] : []), ]; + // Used to determine whether the tx origin is an EOA or a contract + const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; + // Call the sampler contract. const samplerPromise = this._sampler.executeAsync( - this._sampler.getTokenDecimals(makerToken, takerToken), + this._sampler.getTokenDecimals([makerToken, takerToken]), // Get native order fillable amounts. - this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange), + this._sampler.getLimitOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), // Get ETH -> maker token price. this._sampler.getMedianSellRate(feeSourceFilters.sources, makerToken, this._wethAddress, ONE_ETHER), // Get ETH -> taker token price. @@ -180,22 +152,9 @@ export class MarketOperationUtils { takerToken, takerAmount, ), + this._sampler.isAddressContract(txOrigin), ); - const isPriceAwareRfqEnabled = - _opts.rfqt && getPriceAwareRFQRolloutFlags(_opts.rfqt.priceAwareRFQFlag).isIndicativePriceAwareEnabled; - const rfqtPromise = - !isPriceAwareRfqEnabled && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native) - ? getRfqtIndicativeQuotesAsync( - nativeOrders[0].makerAssetData, - nativeOrders[0].takerAssetData, - MarketOperation.Sell, - takerAmount, - undefined, - _opts, - ) - : Promise.resolve([]); - const offChainBalancerPromise = sampleBalancerOffChain ? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) : Promise.resolve([]); @@ -205,56 +164,68 @@ export class MarketOperationUtils { : Promise.resolve([]); const [ - [tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, rawTwoHopQuotes], - rfqtIndicativeQuotes, + [ + tokenDecimals, + orderFillableTakerAmounts, + ethToMakerAssetRate, + ethToTakerAssetRate, + dexQuotes, + rawTwoHopQuotes, + isTxOriginContract, + ], offChainBalancerQuotes, offChainCreamQuotes, - ] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainCreamPromise]); + ] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]); // Filter out any invalid two hop quotes where we couldn't find a route const twoHopQuotes = rawTwoHopQuotes.filter(q => q && q.fillData && q.fillData.firstHopSource); const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals; + + const isRfqSupported = !!(_opts.rfqt && !isTxOriginContract); + const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({ + ...order, + ...getNativeAdjustedFillableAmountsFromTakerAmount(order, orderFillableTakerAmounts[i]), + })); + return { side: MarketOperation.Sell, inputAmount: takerAmount, inputToken: takerToken, outputToken: makerToken, - dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, ...offChainCreamQuotes]), - nativeOrders, - orderFillableAmounts, ethToOutputRate: ethToMakerAssetRate, ethToInputRate: ethToTakerAssetRate, - rfqtIndicativeQuotes, - twoHopQuotes, quoteSourceFilters, makerTokenDecimals: makerTokenDecimals.toNumber(), takerTokenDecimals: takerTokenDecimals.toNumber(), + quotes: { + nativeOrders: limitOrdersWithFillableAmounts, + rfqtIndicativeQuotes: [], + twoHopQuotes, + dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, ...offChainCreamQuotes]), + }, + isRfqSupported, }; } /** * Gets the liquidity available for a market buy operation - * @param nativeOrders Native orders. + * @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders * @param makerAmount Amount of maker asset to buy. * @param opts Options object. * @return MarketSideLiquidity. */ public async getMarketBuyLiquidityAsync( - nativeOrders: SignedOrder[], + nativeOrders: SignedNativeOrder[], makerAmount: BigNumber, opts?: Partial, ): Promise { - if (nativeOrders.length === 0) { - throw new Error(AggregationError.EmptyOrders); - } const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); + const { makerToken, takerToken } = nativeOrders[0].order; const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase); const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources); const quoteSourceFilters = this._buySources.merge(requestFilters); - const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); const { @@ -280,11 +251,14 @@ export class MarketOperationUtils { ...(!sampleBalancerOnChain ? [ERC20BridgeSource.Balancer] : []), ]; + // Used to determine whether the tx origin is an EOA or a contract + const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; + // Call the sampler contract. const samplerPromise = this._sampler.executeAsync( - this._sampler.getTokenDecimals(makerToken, takerToken), + this._sampler.getTokenDecimals([makerToken, takerToken]), // Get native order fillable amounts. - this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange), + this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), // Get ETH -> makerToken token price. this._sampler.getMedianSellRate(feeSourceFilters.sources, makerToken, this._wethAddress, ONE_ETHER), // Get ETH -> taker token price. @@ -302,20 +276,9 @@ export class MarketOperationUtils { takerToken, makerAmount, ), + this._sampler.isAddressContract(txOrigin), ); - const isPriceAwareRfqEnabled = - _opts.rfqt && getPriceAwareRFQRolloutFlags(_opts.rfqt.priceAwareRFQFlag).isIndicativePriceAwareEnabled; - const rfqtPromise = - !isPriceAwareRfqEnabled && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native) - ? getRfqtIndicativeQuotesAsync( - nativeOrders[0].makerAssetData, - nativeOrders[0].takerAssetData, - MarketOperation.Buy, - makerAmount, - undefined, - _opts, - ) - : Promise.resolve([]); + const offChainBalancerPromise = sampleBalancerOffChain ? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) : Promise.resolve([]); @@ -325,79 +288,63 @@ export class MarketOperationUtils { : Promise.resolve([]); const [ - [tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, rawTwoHopQuotes], - rfqtIndicativeQuotes, + [ + tokenDecimals, + orderFillableMakerAmounts, + ethToMakerAssetRate, + ethToTakerAssetRate, + dexQuotes, + rawTwoHopQuotes, + isTxOriginContract, + ], offChainBalancerQuotes, offChainCreamQuotes, - ] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainCreamPromise]); + ] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]); // Filter out any invalid two hop quotes where we couldn't find a route const twoHopQuotes = rawTwoHopQuotes.filter(q => q && q.fillData && q.fillData.firstHopSource); const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals; + const isRfqSupported = !isTxOriginContract; + + const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({ + ...order, + ...getNativeAdjustedFillableAmountsFromMakerAmount(order, orderFillableMakerAmounts[i]), + })); + return { side: MarketOperation.Buy, inputAmount: makerAmount, inputToken: makerToken, outputToken: takerToken, - dexQuotes: dexQuotes.concat(offChainBalancerQuotes, offChainCreamQuotes), - nativeOrders, - orderFillableAmounts, ethToOutputRate: ethToTakerAssetRate, ethToInputRate: ethToMakerAssetRate, - rfqtIndicativeQuotes, - twoHopQuotes, quoteSourceFilters, makerTokenDecimals: makerTokenDecimals.toNumber(), takerTokenDecimals: takerTokenDecimals.toNumber(), + quotes: { + nativeOrders: limitOrdersWithFillableAmounts, + rfqtIndicativeQuotes: [], + twoHopQuotes, + dexQuotes: dexQuotes.concat(offChainBalancerQuotes, offChainCreamQuotes), + }, + isRfqSupported, }; } - /** - * gets the orders required for a market sell operation by (potentially) merging native orders with - * generated bridge orders. - * @param nativeOrders Native orders. - * @param takerAmount Amount of taker asset to sell. - * @param opts Options object. - * @return object with optimized orders and a QuoteReport - */ - public async getMarketSellOrdersAsync( - nativeOrders: SignedOrder[], - takerAmount: BigNumber, - opts?: Partial, - ): Promise { - return this._getMarketSideOrdersAsync(nativeOrders, takerAmount, MarketOperation.Sell, opts); - } - - /** - * gets the orders required for a market buy operation by (potentially) merging native orders with - * generated bridge orders. - * @param nativeOrders Native orders. - * @param makerAmount Amount of maker asset to buy. - * @param opts Options object. - * @return object with optimized orders and a QuoteReport - */ - public async getMarketBuyOrdersAsync( - nativeOrders: SignedOrder[], - makerAmount: BigNumber, - opts?: Partial, - ): Promise { - return this._getMarketSideOrdersAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts); - } - /** * gets the orders required for a batch of market buy operations by (potentially) merging native orders with * generated bridge orders. * * NOTE: Currently `getBatchMarketBuyOrdersAsync()` does not support external liquidity providers. * - * @param batchNativeOrders Batch of Native orders. + * @param batchNativeOrders Batch of Native orders. Assumes LimitOrders not RfqOrders * @param makerAmounts Array amount of maker asset to buy for each batch. * @param opts Options object. * @return orders. */ public async getBatchMarketBuyOrdersAsync( - batchNativeOrders: SignedOrder[][], + batchNativeOrders: SignedNativeOrder[][], makerAmounts: BigNumber[], opts?: Partial, ): Promise> { @@ -413,12 +360,12 @@ export class MarketOperationUtils { const ops = [ ...batchNativeOrders.map(orders => - this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange), + this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy), ), ...batchNativeOrders.map(orders => this._sampler.getMedianSellRate( feeSourceFilters.sources, - getNativeOrderTokens(orders[0])[1], + orders[0].order.takerToken, this._wethAddress, ONE_ETHER, ), @@ -426,18 +373,18 @@ export class MarketOperationUtils { ...batchNativeOrders.map((orders, i) => this._sampler.getBuyQuotes( quoteSourceFilters.sources, - getNativeOrderTokens(orders[0])[0], - getNativeOrderTokens(orders[0])[1], + orders[0].order.makerToken, + orders[0].order.takerToken, [makerAmounts[i]], ), ), ...batchNativeOrders.map(orders => - this._sampler.getTokenDecimals(getNativeOrderTokens(orders[0])[0], getNativeOrderTokens(orders[0])[1]), + this._sampler.getTokenDecimals([orders[0].order.makerToken, orders[0].order.takerToken]), ), ]; const executeResults = await this._sampler.executeBatchAsync(ops); - const batchOrderFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][]; + const batchOrderFillableMakerAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][]; const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[]; const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][]; const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][]; @@ -448,8 +395,8 @@ export class MarketOperationUtils { if (nativeOrders.length === 0) { throw new Error(AggregationError.EmptyOrders); } - const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); - const orderFillableAmounts = batchOrderFillableAmounts[i]; + const { makerToken, takerToken } = nativeOrders[0].order; + const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i]; const ethToTakerAssetRate = batchEthToTakerAssetRate[i]; const dexQuotes = batchDexQuotes[i]; const makerAmount = makerAmounts[i]; @@ -457,19 +404,24 @@ export class MarketOperationUtils { const optimizerResult = await this._generateOptimizedOrdersAsync( { side: MarketOperation.Buy, - nativeOrders, - orderFillableAmounts, - dexQuotes, + inputToken: makerToken, + outputToken: takerToken, inputAmount: makerAmount, ethToOutputRate: ethToTakerAssetRate, ethToInputRate, - rfqtIndicativeQuotes: [], - inputToken: makerToken, - outputToken: takerToken, - twoHopQuotes: [], quoteSourceFilters, makerTokenDecimals: batchTokenDecimals[i][0], takerTokenDecimals: batchTokenDecimals[i][1], + quotes: { + nativeOrders: nativeOrders.map((o, k) => ({ + ...o, + ...getNativeAdjustedFillableAmountsFromMakerAmount(o, orderFillableMakerAmounts[k]), + })), + dexQuotes, + rfqtIndicativeQuotes: [], + twoHopQuotes: [], + }, + isRfqSupported: false, }, { bridgeSlippage: _opts.bridgeSlippage, @@ -498,13 +450,11 @@ export class MarketOperationUtils { outputToken, side, inputAmount, - nativeOrders, - orderFillableAmounts, - rfqtIndicativeQuotes, - dexQuotes, + quotes, ethToOutputRate, ethToInputRate, } = marketSideLiquidity; + const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes; const maxFallbackSlippage = opts.maxFallbackSlippage || 0; const orderOpts = { @@ -516,14 +466,23 @@ export class MarketOperationUtils { bridgeSlippage: opts.bridgeSlippage || 0, }; + const augmentedRfqtIndicativeQuotes: NativeOrderWithFillableAmounts[] = rfqtIndicativeQuotes.map( + q => + // tslint:disable-next-line: no-object-literal-type-assertion + ({ + order: { ...new RfqOrder({ ...q }) }, + signature: INVALID_SIGNATURE, + fillableMakerAmount: new BigNumber(q.makerAmount), + fillableTakerAmount: new BigNumber(q.takerAmount), + fillableTakerFeeAmount: ZERO_AMOUNT, + type: FillQuoteTransformerOrderType.Rfq, + } as NativeOrderWithFillableAmounts), + ); + // Convert native orders and dex quotes into `Fill` objects. const fills = createFills({ side, - // Augment native orders with their fillable amounts. - orders: [ - ...createSignedOrdersWithFillableAmounts(side, nativeOrders, orderFillableAmounts), - ...createSignedOrdersFromRfqtIndicativeQuotes(rfqtIndicativeQuotes), - ], + orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes], dexQuotes, targetInput: inputAmount, ethToOutputRate, @@ -540,12 +499,8 @@ export class MarketOperationUtils { }; // NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset - const takerAssetToEthRate = side === MarketOperation.Sell ? ethToInputRate : ethToOutputRate; - const makerAssetToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate; - - // Find the unoptimized best rate to calculate savings from optimizer - const _unoptimizedPath = fillsToSortedPaths(fills, side, inputAmount, optimizerOpts)[0]; - const unoptimizedPath = _unoptimizedPath ? _unoptimizedPath.collapse(orderOpts) : undefined; + const takerTokenToEthRate = side === MarketOperation.Sell ? ethToInputRate : ethToOutputRate; + const makerTokenToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate; // Find the optimal path const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); @@ -564,9 +519,8 @@ export class MarketOperationUtils { sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], marketSideLiquidity, adjustedRate: bestTwoHopRate, - unoptimizedPath, - takerAssetToEthRate, - makerAssetToEthRate, + takerTokenToEthRate, + makerTokenToEthRate, }; } @@ -599,14 +553,16 @@ export class MarketOperationUtils { sourceFlags: collapsedPath.sourceFlags, marketSideLiquidity, adjustedRate: optimalPathRate, - unoptimizedPath, - takerAssetToEthRate, - makerAssetToEthRate, + takerTokenToEthRate, + makerTokenToEthRate, }; } - private async _getMarketSideOrdersAsync( - nativeOrders: SignedOrder[], + /** + * @param nativeOrders: Assumes LimitOrders not RfqOrders + */ + public async getOptimizerResultAsync( + nativeOrders: SignedNativeOrder[], amount: BigNumber, side: MarketOperation, opts?: Partial, @@ -621,12 +577,16 @@ export class MarketOperationUtils { exchangeProxyOverhead: _opts.exchangeProxyOverhead, }; + if (nativeOrders.length === 0) { + throw new Error(AggregationError.EmptyOrders); + } + // Compute an optimized path for on-chain DEX and open-orderbook. This should not include RFQ liquidity. const marketLiquidityFnAsync = side === MarketOperation.Sell ? this.getMarketSellLiquidityAsync.bind(this) : this.getMarketBuyLiquidityAsync.bind(this); - let marketSideLiquidity: MarketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts); + const marketSideLiquidity: MarketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts); let optimizerResult: OptimizerResult | undefined; try { optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); @@ -639,34 +599,39 @@ export class MarketOperationUtils { } } - // If RFQ liquidity is enabled, make a request to check RFQ liquidity + // Calculate a suggested price. For now, this is simply the overall price of the aggregation. + // We can use this as a comparison price for RFQ let wholeOrderPrice: BigNumber | undefined; + if (optimizerResult) { + wholeOrderPrice = getComparisonPrices( + optimizerResult.adjustedRate, + amount, + marketSideLiquidity, + _opts.feeSchedule, + ).wholeOrder; + } + + // If RFQ liquidity is enabled, make a request to check RFQ liquidity against the first optimizer result const { rfqt } = _opts; - if (rfqt && rfqt.quoteRequestor && marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)) { - // Calculate a suggested price. For now, this is simply the overall price of the aggregation. - if (optimizerResult) { - wholeOrderPrice = getComparisonPrices( - optimizerResult.adjustedRate, + if ( + marketSideLiquidity.isRfqSupported && + rfqt && + rfqt.quoteRequestor && + marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native) + ) { + // Timing of RFQT lifecycle + const timeStart = new Date().getTime(); + const { makerToken, takerToken } = nativeOrders[0].order; + if (rfqt.isIndicative) { + // An indicative quote is being requested, and indicative quotes price-aware enabled + // Make the RFQT request and then re-run the sampler if new orders come back. + const indicativeQuotes = await rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( + makerToken, + takerToken, amount, - marketSideLiquidity, - _opts.feeSchedule, - ).wholeOrder; - } - - const { isFirmPriceAwareEnabled, isIndicativePriceAwareEnabled } = getPriceAwareRFQRolloutFlags( - rfqt.priceAwareRFQFlag, - ); - - if (rfqt.isIndicative && isIndicativePriceAwareEnabled) { - // An indicative quote is beingh requested, and indicative quotes price-aware enabled. Make the RFQT request and then re-run the sampler if new orders come back. - const timeStart = new Date().getTime(); - const indicativeQuotes = await getRfqtIndicativeQuotesAsync( - nativeOrders[0].makerAssetData, - nativeOrders[0].takerAssetData, side, - amount, wholeOrderPrice, - _opts, + rfqt, ); const deltaTime = new Date().getTime() - timeStart; DEFAULT_INFO_LOGGER({ @@ -675,57 +640,57 @@ export class MarketOperationUtils { }); // Re-run optimizer with the new indicative quote if (indicativeQuotes.length > 0) { - marketSideLiquidity = { - ...marketSideLiquidity, - rfqtIndicativeQuotes: indicativeQuotes, - }; + marketSideLiquidity.quotes.rfqtIndicativeQuotes = indicativeQuotes; optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); } - } else if (!rfqt.isIndicative && isFirmPriceAwareEnabled) { - // A firm quote is being requested, and firm quotes price-aware enabled. Ensure that `intentOnFilling` is enabled. - if (rfqt.intentOnFilling) { - // Extra validation happens when requesting a firm quote, such as ensuring that the takerAddress - // is indeed valid. - if (!rfqt.takerAddress || rfqt.takerAddress === NULL_ADDRESS) { - throw new Error('RFQ-T requests must specify a taker address'); - } - const timeStart = new Date().getTime(); - const firmQuotes = await rfqt.quoteRequestor.requestRfqtFirmQuotesAsync( - nativeOrders[0].makerAssetData, - nativeOrders[0].takerAssetData, - amount, - side, - wholeOrderPrice, - rfqt, - ); - const deltaTime = new Date().getTime() - timeStart; - DEFAULT_INFO_LOGGER({ - rfqQuoteType: 'firm', - deltaTime, - }); - if (firmQuotes.length > 0) { - // Compute the RFQ order fillable amounts. This is done by performing a "soft" order - // validation and by checking order balances that are monitored by our worker. - // If a firm quote validator does not exist, then we assume that all orders are valid. - const firmQuoteSignedOrders = firmQuotes.map(quote => quote.signedOrder); - const rfqOrderFillableAmounts = - rfqt.firmQuoteValidator === undefined - ? firmQuoteSignedOrders.map(signedOrder => signedOrder.takerAssetAmount) - : await rfqt.firmQuoteValidator.getRfqtTakerFillableAmountsAsync(firmQuoteSignedOrders); + } else { + // A firm quote is being requested, and firm quotes price-aware enabled. + // Ensure that `intentOnFilling` is enabled and make the request. + const firmQuotes = await rfqt.quoteRequestor.requestRfqtFirmQuotesAsync( + makerToken, + takerToken, + amount, + side, + wholeOrderPrice, + rfqt, + ); + const deltaTime = new Date().getTime() - timeStart; + DEFAULT_INFO_LOGGER({ + rfqQuoteType: 'firm', + deltaTime, + }); + if (firmQuotes.length > 0) { + // Compute the RFQ order fillable amounts. This is done by performing a "soft" order + // validation and by checking order balances that are monitored by our worker. + // If a firm quote validator does not exist, then we assume that all orders are valid. + const rfqTakerFillableAmounts = + rfqt.firmQuoteValidator === undefined + ? firmQuotes.map(signedOrder => signedOrder.order.takerAmount) + : await rfqt.firmQuoteValidator.getRfqtTakerFillableAmountsAsync( + firmQuotes.map(q => new RfqOrder(q.order)), + ); - marketSideLiquidity = { - ...marketSideLiquidity, - nativeOrders: marketSideLiquidity.nativeOrders.concat(firmQuoteSignedOrders), - orderFillableAmounts: marketSideLiquidity.orderFillableAmounts.concat( - rfqOrderFillableAmounts, + const quotesWithOrderFillableAmounts: NativeOrderWithFillableAmounts[] = firmQuotes.map( + (order, i) => ({ + ...order, + fillableTakerAmount: rfqTakerFillableAmounts[i], + // Adjust the maker amount by the available taker fill amount + fillableMakerAmount: getNativeAdjustedMakerFillAmount( + order.order, + rfqTakerFillableAmounts[i], ), - }; + fillableTakerFeeAmount: ZERO_AMOUNT, + }), + ); + marketSideLiquidity.quotes.nativeOrders = [ + ...quotesWithOrderFillableAmounts, + ...marketSideLiquidity.quotes.nativeOrders, + ]; - // Re-run optimizer with the new firm quote. This is the second and last time - // we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception - // and we let it bubble up if it happens. - optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); - } + // Re-run optimizer with the new firm quote. This is the second and last time + // we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception + // and we let it bubble up if it happens. + optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); } } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts index 96678b7dd0..6b064fd159 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts @@ -39,7 +39,8 @@ export function getBestTwoHopQuote( feeSchedule?: FeeSchedule, exchangeProxyOverhead?: ExchangeProxyOverhead, ): { quote: DexSample | undefined; adjustedRate: BigNumber } { - const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity; + const { side, inputAmount, ethToOutputRate, quotes } = marketSideLiquidity; + const { twoHopQuotes } = quotes; // Ensure the expected data we require exists. In the case where all hops reverted // or there were no sources included that allowed for multi hop, // we can end up with empty, but not undefined, fill data diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index e4277e0ba8..b2305ae33b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,18 +1,15 @@ -import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; -import { V3RFQIndicativeQuote } from '@0x/quote-server'; -import { SignedOrder } from '@0x/types'; +import { BridgeSource, FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { AbiEncoder, BigNumber } from '@0x/utils'; -import { AssetSwapperContractAddresses, MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; +import { AssetSwapperContractAddresses, MarketOperation } from '../../types'; import { - ERC20_PROXY_ID, + MAINNET_DODO_HELPER, + MAINNET_KYBER_NETWORK_PROXY, + MAINNET_MSTABLE_ROUTER, + MAINNET_OASIS_ROUTER, + MAINNET_UNISWAP_V1_ROUTER, MAX_UINT256, - NULL_ADDRESS, - NULL_BYTES, - ONE_HOUR_IN_SECONDS, - ONE_SECOND_MS, - WALLET_SIGNATURE, ZERO_AMOUNT, } from './constants'; import { @@ -29,7 +26,11 @@ import { MooniswapFillData, MultiHopFillData, NativeCollapsedFill, + NativeLimitOrderFillData, + NativeRfqOrderFillData, + OptimizedMarketBridgeOrder, OptimizedMarketOrder, + OptimizedMarketOrderBase, OrderDomain, ShellFillData, SnowSwapFillData, @@ -38,95 +39,7 @@ import { UniswapV2FillData, } from './types'; -// tslint:disable completed-docs no-unnecessary-type-assertion - -export function createDummyOrderForSampler( - makerAssetData: string, - takerAssetData: string, - makerAddress: string, -): SignedOrder { - return { - makerAddress, - takerAddress: NULL_ADDRESS, - senderAddress: NULL_ADDRESS, - feeRecipientAddress: NULL_ADDRESS, - salt: ZERO_AMOUNT, - expirationTimeSeconds: ZERO_AMOUNT, - makerAssetData, - takerAssetData, - makerFeeAssetData: NULL_BYTES, - takerFeeAssetData: NULL_BYTES, - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, - makerAssetAmount: ZERO_AMOUNT, - takerAssetAmount: ZERO_AMOUNT, - signature: NULL_BYTES, - chainId: 1, - exchangeAddress: NULL_ADDRESS, - }; -} - -export function getNativeOrderTokens(order: SignedOrder): [string, string] { - const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [ - ERC20AssetData, - ERC20AssetData - ]; - if (assets.some(a => a.assetProxyId !== ERC20_PROXY_ID)) { - throw new Error(AggregationError.NotERC20AssetData); - } - return assets.map(a => a.tokenAddress.toLowerCase()) as [string, string]; -} - -export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder { - return { - ...order, - fillableMakerAssetAmount: order.makerAssetAmount, - fillableTakerAssetAmount: order.takerAssetAmount, - fillableTakerFeeAmount: order.takerFee, - fills: [], - }; -} - -/** - * Augments native orders with fillable amounts and filters out unfillable orders. - */ -export function createSignedOrdersWithFillableAmounts( - side: MarketOperation, - orders: SignedOrder[], - fillableAmounts: BigNumber[], -): SignedOrderWithFillableAmounts[] { - // Quick safety check: ensures that orders maps perfectly to fillable amounts. - if (orders.length !== fillableAmounts.length) { - throw new Error( - `Number of orders was ${orders.length} but fillable amounts was ${ - fillableAmounts.length - }. This should never happen`, - ); - } - - return orders - .map((order: SignedOrder, i: number) => { - const fillableAmount = fillableAmounts[i]; - const fillableMakerAssetAmount = - side === MarketOperation.Buy - ? fillableAmount - : orderCalculationUtils.getMakerFillAmount(order, fillableAmount); - const fillableTakerAssetAmount = - side === MarketOperation.Sell - ? fillableAmount - : orderCalculationUtils.getTakerFillAmount(order, fillableAmount); - const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount); - return { - ...order, - fillableMakerAssetAmount, - fillableTakerAssetAmount, - fillableTakerFeeAmount, - }; - }) - .filter(order => { - return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero(); - }); -} +// tslint:disable completed-docs export interface CreateOrderFromPathOpts { side: MarketOperation; @@ -142,10 +55,11 @@ export function createOrdersFromTwoHopSample( opts: CreateOrderFromPathOpts, ): OptimizedMarketOrder[] { const [makerToken, takerToken] = getMakerTakerTokens(opts); - const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData!; + const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData; const firstHopFill: CollapsedFill = { sourcePathId: '', source: firstHopSource.source, + type: FillQuoteTransformerOrderType.Bridge, input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT, output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output, subFills: [], @@ -154,209 +68,158 @@ export function createOrdersFromTwoHopSample( const secondHopFill: CollapsedFill = { sourcePathId: '', source: secondHopSource.source, + type: FillQuoteTransformerOrderType.Bridge, input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input, output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256, subFills: [], fillData: secondHopSource.fillData, }; return [ - createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts), - createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts), + createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side), + createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts.side), ]; } -function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string { - switch (fill.source) { - case ERC20BridgeSource.Eth2Dai: - return opts.contractAddresses.eth2DaiBridge; - case ERC20BridgeSource.Kyber: - return opts.contractAddresses.kyberBridge; - case ERC20BridgeSource.Uniswap: - return opts.contractAddresses.uniswapBridge; - case ERC20BridgeSource.UniswapV2: - return opts.contractAddresses.uniswapV2Bridge; - case ERC20BridgeSource.SushiSwap: - return opts.contractAddresses.sushiswapBridge; - case ERC20BridgeSource.Curve: - return opts.contractAddresses.curveBridge; - case ERC20BridgeSource.Swerve: - return opts.contractAddresses.swerveBridge; - case ERC20BridgeSource.SnowSwap: - return opts.contractAddresses.snowswapBridge; - case ERC20BridgeSource.Bancor: - return opts.contractAddresses.bancorBridge; +export function getERC20BridgeSourceToBridgeSource(source: ERC20BridgeSource): BridgeSource { + switch (source) { case ERC20BridgeSource.Balancer: - return opts.contractAddresses.balancerBridge; + return BridgeSource.Balancer; + case ERC20BridgeSource.Bancor: + return BridgeSource.Bancor; + // case ERC20BridgeSource.CoFiX: + // return BridgeSource.CoFiX; + case ERC20BridgeSource.Curve: + return BridgeSource.Curve; case ERC20BridgeSource.Cream: - return opts.contractAddresses.creamBridge; - case ERC20BridgeSource.LiquidityProvider: - return (fill.fillData as LiquidityProviderFillData).poolAddress; - case ERC20BridgeSource.MStable: - return opts.contractAddresses.mStableBridge; - case ERC20BridgeSource.Mooniswap: - return opts.contractAddresses.mooniswapBridge; - case ERC20BridgeSource.Shell: - return opts.contractAddresses.shellBridge; - case ERC20BridgeSource.Dodo: - return opts.contractAddresses.dodoBridge; + return BridgeSource.Cream; case ERC20BridgeSource.CryptoCom: - return opts.contractAddresses.cryptoComBridge; + return BridgeSource.CryptoCom; + case ERC20BridgeSource.Dodo: + return BridgeSource.Dodo; + case ERC20BridgeSource.Kyber: + return BridgeSource.Kyber; + case ERC20BridgeSource.LiquidityProvider: + return BridgeSource.LiquidityProvider; + case ERC20BridgeSource.Mooniswap: + return BridgeSource.Mooniswap; + case ERC20BridgeSource.MStable: + return BridgeSource.MStable; + case ERC20BridgeSource.Eth2Dai: + return BridgeSource.Oasis; + case ERC20BridgeSource.Shell: + return BridgeSource.Shell; + case ERC20BridgeSource.SnowSwap: + return BridgeSource.Snowswap; + case ERC20BridgeSource.SushiSwap: + return BridgeSource.Sushiswap; + case ERC20BridgeSource.Swerve: + return BridgeSource.Swerve; + case ERC20BridgeSource.Uniswap: + return BridgeSource.Uniswap; + case ERC20BridgeSource.UniswapV2: + return BridgeSource.UniswapV2; default: - break; + throw new Error(AggregationError.NoBridgeForSource); } - throw new Error(AggregationError.NoBridgeForSource); +} + +export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder): string { + let bridgeData: string; + if ( + order.source === ERC20BridgeSource.MultiHop || + order.source === ERC20BridgeSource.MultiBridge || + order.source === ERC20BridgeSource.Native + ) { + throw new Error('Invalid order to encode for Bridge Data'); + } + const encoder = BRIDGE_ENCODERS[order.source]; + + if (!encoder) { + throw new Error(AggregationError.NoBridgeForSource); + } + + switch (order.source) { + case ERC20BridgeSource.Curve: + case ERC20BridgeSource.Swerve: + case ERC20BridgeSource.SnowSwap: + const curveFillData = (order as OptimizedMarketBridgeOrder< + CurveFillData | SwerveFillData | SnowSwapFillData + >).fillData; + bridgeData = encoder.encode([ + curveFillData.pool.poolAddress, + curveFillData.pool.exchangeFunctionSelector, + curveFillData.fromTokenIdx, + curveFillData.toTokenIdx, + ]); + break; + case ERC20BridgeSource.Balancer: + case ERC20BridgeSource.Cream: + const balancerFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([balancerFillData.poolAddress]); + break; + case ERC20BridgeSource.Bancor: + const bancorFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([bancorFillData.networkAddress, bancorFillData.path]); + break; + case ERC20BridgeSource.UniswapV2: + case ERC20BridgeSource.SushiSwap: + case ERC20BridgeSource.CryptoCom: + const uniswapV2FillData = (order as OptimizedMarketBridgeOrder) + .fillData; + bridgeData = encoder.encode([uniswapV2FillData.router, uniswapV2FillData.tokenAddressPath]); + break; + case ERC20BridgeSource.Kyber: + const kyberFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([MAINNET_KYBER_NETWORK_PROXY, kyberFillData.hint]); + break; + case ERC20BridgeSource.Mooniswap: + const mooniswapFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([mooniswapFillData.poolAddress]); + break; + case ERC20BridgeSource.Dodo: + const dodoFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([MAINNET_DODO_HELPER, dodoFillData.poolAddress, dodoFillData.isSellBase]); + break; + case ERC20BridgeSource.Shell: + const shellFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([shellFillData.poolAddress]); + break; + case ERC20BridgeSource.LiquidityProvider: + const lpFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([lpFillData.poolAddress, tokenAddressEncoder.encode([order.takerToken])]); + break; + case ERC20BridgeSource.Uniswap: + bridgeData = encoder.encode([MAINNET_UNISWAP_V1_ROUTER]); + break; + case ERC20BridgeSource.Eth2Dai: + bridgeData = encoder.encode([MAINNET_OASIS_ROUTER]); + break; + case ERC20BridgeSource.MStable: + bridgeData = encoder.encode([MAINNET_MSTABLE_ROUTER]); + break; + default: + throw new Error(AggregationError.NoBridgeForSource); + } + return bridgeData; } export function createBridgeOrder( fill: CollapsedFill, makerToken: string, takerToken: string, - opts: CreateOrderFromPathOpts, -): OptimizedMarketOrder { - const bridgeAddress = getBridgeAddressFromFill(fill, opts); - - let makerAssetData; - switch (fill.source) { - case ERC20BridgeSource.Curve: - const curveFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createCurveBridgeData( - curveFillData.pool.poolAddress, - curveFillData.pool.exchangeFunctionSelector, - takerToken, - curveFillData.fromTokenIdx, - curveFillData.toTokenIdx, - ), - ); - break; - case ERC20BridgeSource.Swerve: - const swerveFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createCurveBridgeData( - swerveFillData.pool.poolAddress, - swerveFillData.pool.exchangeFunctionSelector, - takerToken, - swerveFillData.fromTokenIdx, - swerveFillData.toTokenIdx, - ), - ); - break; - case ERC20BridgeSource.SnowSwap: - const snowSwapFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createCurveBridgeData( - snowSwapFillData.pool.poolAddress, - snowSwapFillData.pool.exchangeFunctionSelector, - takerToken, - snowSwapFillData.fromTokenIdx, - snowSwapFillData.toTokenIdx, - ), - ); - break; - case ERC20BridgeSource.Balancer: - const balancerFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createBalancerBridgeData(takerToken, balancerFillData.poolAddress), - ); - break; - case ERC20BridgeSource.Cream: - const creamFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createBalancerBridgeData(takerToken, creamFillData.poolAddress), - ); - break; - case ERC20BridgeSource.Bancor: - const bancorFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createBancorBridgeData(bancorFillData.path, bancorFillData.networkAddress), - ); - break; - case ERC20BridgeSource.UniswapV2: - const uniswapV2FillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createUniswapV2BridgeData(uniswapV2FillData.tokenAddressPath), - ); - break; - case ERC20BridgeSource.SushiSwap: - const sushiSwapFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createSushiSwapBridgeData(sushiSwapFillData.tokenAddressPath, sushiSwapFillData.router), - ); - break; - case ERC20BridgeSource.CryptoCom: - const cryptoComFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createSushiSwapBridgeData(cryptoComFillData.tokenAddressPath, cryptoComFillData.router), - ); - break; - case ERC20BridgeSource.Kyber: - const kyberFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createKyberBridgeData(takerToken, kyberFillData.hint), - ); - break; - case ERC20BridgeSource.Mooniswap: - const mooniswapFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createMooniswapBridgeData(takerToken, mooniswapFillData.poolAddress), - ); - break; - case ERC20BridgeSource.Dodo: - const dodoFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createDODOBridgeData(takerToken, dodoFillData.poolAddress, dodoFillData.isSellBase), - ); - break; - case ERC20BridgeSource.Shell: - const shellFillData = (fill as CollapsedFill).fillData!; // tslint:disable-line:no-non-null-assertion - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createShellBridgeData(takerToken, shellFillData.poolAddress), - ); - break; - default: - makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - bridgeAddress, - createBridgeData(takerToken), - ); - } - const [slippedMakerAssetAmount, slippedTakerAssetAmount] = getSlippedBridgeAssetAmounts(fill, opts); + side: MarketOperation, +): OptimizedMarketBridgeOrder { + const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side); return { + makerToken, + takerToken, + makerAmount, + takerAmount, + fillData: fill.fillData, + source: fill.source, + sourcePathId: fill.sourcePathId, + type: FillQuoteTransformerOrderType.Bridge, fills: [fill], - makerAssetData, - takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), - makerAddress: bridgeAddress, - makerAssetAmount: slippedMakerAssetAmount, - takerAssetAmount: slippedTakerAssetAmount, - fillableMakerAssetAmount: slippedMakerAssetAmount, - fillableTakerAssetAmount: slippedTakerAssetAmount, - ...createCommonBridgeOrderFields(opts.orderDomain), }; } @@ -366,162 +229,80 @@ export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, str return [makerToken, takerToken]; } -function createBridgeData(tokenAddress: string): string { - const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]); - return encoder.encode({ tokenAddress }); -} +const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]); +const curveEncoder = AbiEncoder.create([ + { name: 'curveAddress', type: 'address' }, + { name: 'exchangeFunctionSelector', type: 'bytes4' }, + { name: 'fromTokenIdx', type: 'int128' }, + { name: 'toTokenIdx', type: 'int128' }, +]); +const routerAddressPathEncoder = AbiEncoder.create('(address,address[])'); +const tokenAddressEncoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]); -function createBalancerBridgeData(takerToken: string, poolAddress: string): string { - const encoder = AbiEncoder.create([ - { name: 'takerToken', type: 'address' }, - { name: 'poolAddress', type: 'address' }, - ]); - return encoder.encode({ takerToken, poolAddress }); -} - -function createShellBridgeData(takerToken: string, poolAddress: string): string { - const encoder = AbiEncoder.create([ - { name: 'takerToken', type: 'address' }, - { name: 'poolAddress', type: 'address' }, - ]); - return encoder.encode({ takerToken, poolAddress }); -} - -function createBancorBridgeData(path: string[], networkAddress: string): string { - const encoder = AbiEncoder.create([ - { name: 'path', type: 'address[]' }, - { name: 'networkAddress', type: 'address' }, - ]); - return encoder.encode({ path, networkAddress }); -} - -function createKyberBridgeData(fromTokenAddress: string, hint: string): string { - const encoder = AbiEncoder.create([{ name: 'fromTokenAddress', type: 'address' }, { name: 'hint', type: 'bytes' }]); - return encoder.encode({ fromTokenAddress, hint }); -} - -function createMooniswapBridgeData(takerToken: string, poolAddress: string): string { - const encoder = AbiEncoder.create([ - { name: 'takerToken', type: 'address' }, - { name: 'poolAddress', type: 'address' }, - ]); - return encoder.encode({ takerToken, poolAddress }); -} - -function createDODOBridgeData(takerToken: string, poolAddress: string, isSellBase: boolean): string { - const encoder = AbiEncoder.create([ - { name: 'takerToken', type: 'address' }, +export const BRIDGE_ENCODERS: { + [key in Exclude< + ERC20BridgeSource, + ERC20BridgeSource.Native | ERC20BridgeSource.MultiHop | ERC20BridgeSource.MultiBridge + >]: AbiEncoder.DataType +} = { + [ERC20BridgeSource.LiquidityProvider]: AbiEncoder.create([ + { name: 'provider', type: 'address' }, + { name: 'data', type: 'bytes' }, + ]), + [ERC20BridgeSource.Kyber]: AbiEncoder.create([ + { name: 'kyberNetworkProxy', type: 'address' }, + { name: 'hint', type: 'bytes' }, + ]), + [ERC20BridgeSource.Dodo]: AbiEncoder.create([ + { name: 'helper', type: 'address' }, { name: 'poolAddress', type: 'address' }, { name: 'isSellBase', type: 'bool' }, - ]); - return encoder.encode({ takerToken, poolAddress, isSellBase }); -} + ]), + // Curve like + [ERC20BridgeSource.Curve]: curveEncoder, + [ERC20BridgeSource.Swerve]: curveEncoder, + [ERC20BridgeSource.SnowSwap]: curveEncoder, + // UniswapV2 like, (router, address[]) + [ERC20BridgeSource.Bancor]: routerAddressPathEncoder, + [ERC20BridgeSource.UniswapV2]: routerAddressPathEncoder, + [ERC20BridgeSource.SushiSwap]: routerAddressPathEncoder, + [ERC20BridgeSource.CryptoCom]: routerAddressPathEncoder, + // Generic pools + [ERC20BridgeSource.Shell]: poolEncoder, + [ERC20BridgeSource.Mooniswap]: poolEncoder, + [ERC20BridgeSource.Eth2Dai]: poolEncoder, + [ERC20BridgeSource.MStable]: poolEncoder, + [ERC20BridgeSource.Balancer]: poolEncoder, + [ERC20BridgeSource.Cream]: poolEncoder, + [ERC20BridgeSource.Uniswap]: poolEncoder, +}; -function createCurveBridgeData( - curveAddress: string, - exchangeFunctionSelector: string, - takerToken: string, - fromTokenIdx: number, - toTokenIdx: number, -): string { - const encoder = AbiEncoder.create([ - { name: 'curveAddress', type: 'address' }, - { name: 'exchangeFunctionSelector', type: 'bytes4' }, - { name: 'fromTokenAddress', type: 'address' }, - { name: 'fromTokenIdx', type: 'int128' }, - { name: 'toTokenIdx', type: 'int128' }, - ]); - return encoder.encode([curveAddress, exchangeFunctionSelector, takerToken, fromTokenIdx, toTokenIdx]); -} - -function createUniswapV2BridgeData(tokenAddressPath: string[]): string { - const encoder = AbiEncoder.create('(address[])'); - return encoder.encode([tokenAddressPath]); -} - -function createSushiSwapBridgeData(tokenAddressPath: string[], router: string): string { - const encoder = AbiEncoder.create('(address[],address)'); - return encoder.encode([tokenAddressPath, router]); -} - -function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFromPathOpts): [BigNumber, BigNumber] { +function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] { return [ // Maker asset amount. - opts.side === MarketOperation.Sell - ? fill.output.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN) - : fill.input, + side === MarketOperation.Sell ? fill.output : fill.input, // Taker asset amount. - opts.side === MarketOperation.Sell - ? fill.input - : BigNumber.min(fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), MAX_UINT256), + side === MarketOperation.Sell ? fill.input : fill.output, ]; } -type CommonBridgeOrderFields = Pick< - OptimizedMarketOrder, - Exclude< - keyof OptimizedMarketOrder, - | 'fills' - | 'makerAddress' - | 'makerAssetData' - | 'takerAssetData' - | 'makerAssetAmount' - | 'takerAssetAmount' - | 'fillableMakerAssetAmount' - | 'fillableTakerAssetAmount' - > ->; - -function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOrderFields { - return { - takerAddress: NULL_ADDRESS, - senderAddress: NULL_ADDRESS, - feeRecipientAddress: NULL_ADDRESS, - salt: generatePseudoRandomSalt(), - // 2 hours from now - expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS * 2), - makerFeeAssetData: NULL_BYTES, - takerFeeAssetData: NULL_BYTES, - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, - fillableTakerFeeAmount: ZERO_AMOUNT, - signature: WALLET_SIGNATURE, - ...orderDomain, - }; -} - -export function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { - return { +export function createNativeOptimizedOrder( + fill: NativeCollapsedFill, + side: MarketOperation, +): OptimizedMarketOrderBase | OptimizedMarketOrderBase { + const fillData = fill.fillData; + const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side); + const base = { + type: fill.type, + source: ERC20BridgeSource.Native, + makerToken: fillData.order.makerToken, + takerToken: fillData.order.takerToken, + makerAmount, + takerAmount, fills: [fill], - ...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion + fillData, }; -} - -export function createSignedOrdersFromRfqtIndicativeQuotes( - quotes: V3RFQIndicativeQuote[], -): SignedOrderWithFillableAmounts[] { - return quotes.map(quote => { - return { - fillableMakerAssetAmount: quote.makerAssetAmount, - fillableTakerAssetAmount: quote.takerAssetAmount, - makerAssetAmount: quote.makerAssetAmount, - takerAssetAmount: quote.takerAssetAmount, - makerAssetData: quote.makerAssetData, - takerAssetData: quote.takerAssetData, - takerAddress: NULL_ADDRESS, - makerAddress: NULL_ADDRESS, - senderAddress: NULL_ADDRESS, - feeRecipientAddress: NULL_ADDRESS, - salt: ZERO_AMOUNT, - expirationTimeSeconds: quote.expirationTimeSeconds, - makerFeeAssetData: NULL_BYTES, - takerFeeAssetData: NULL_BYTES, - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, - fillableTakerFeeAmount: ZERO_AMOUNT, - signature: WALLET_SIGNATURE, - chainId: 0, - exchangeAddress: NULL_ADDRESS, - }; - }); + return fill.type === FillQuoteTransformerOrderType.Rfq + ? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData } + : { ...base, type: FillQuoteTransformerOrderType.Limit, fillData: fillData as NativeLimitOrderFillData }; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts index d8f1140dac..5788c0a73b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils'; import { MarketOperation } from '../../types'; import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; -import { createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders'; +import { createBridgeOrder, createNativeOptimizedOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders'; import { getCompleteRate, getRate } from './rate_utils'; import { CollapsedFill, @@ -105,11 +105,12 @@ export class Path { this.orders = []; for (let i = 0; i < collapsedFills.length; ) { if (collapsedFills[i].source === ERC20BridgeSource.Native) { - this.orders.push(createNativeOrder(collapsedFills[i] as NativeCollapsedFill)); + this.orders.push(createNativeOptimizedOrder(collapsedFills[i] as NativeCollapsedFill, opts.side)); ++i; continue; } // If there are contiguous bridge orders, we can batch them together. + // TODO jacob pretty sure this is from DFB and we can remove const contiguousBridgeFills = [collapsedFills[i]]; for (let j = i + 1; j < collapsedFills.length; ++j) { if (collapsedFills[j].source === ERC20BridgeSource.Native) { @@ -118,7 +119,7 @@ export class Path { contiguousBridgeFills.push(collapsedFills[j]); } - this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); + this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts.side)); i += 1; } return this as CollapsedPath; @@ -236,6 +237,7 @@ export class Path { (this.collapsedFills as CollapsedFill[]).push({ sourcePathId: fill.sourcePathId, source: fill.source, + type: fill.type, fillData: fill.fillData, input: fill.input, output: fill.output, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts index f560e976c3..54468643ae 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts @@ -3,7 +3,7 @@ import { BigNumber, decodeBytesAsRevertError, logUtils } from '@0x/utils'; import { ERC20BridgeSamplerContract } from '../../wrappers'; -import { ERC20BridgeSource, FillData, SourceInfo, SourceQuoteOperation } from './types'; +import { ERC20BridgeSource, FillData, SourceQuoteOperation } from './types'; export type Parameters = T extends (...args: infer TArgs) => any ? TArgs : never; @@ -28,7 +28,7 @@ export class SamplerContractOperation< private readonly _params: Parameters; private readonly _callback?: (callResults: string, fillData: TFillData) => BigNumber[]; - constructor(opts: SourceInfo & SamplerContractCall) { + constructor(opts: { source: ERC20BridgeSource; fillData?: TFillData } & SamplerContractCall) { this.source = opts.source; this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion this._samplerContract = opts.contract; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index 49e3e22a2a..35256ef1aa 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -1,8 +1,8 @@ -import { SignedOrder } from '@0x/types'; +import { LimitOrderFields } from '@0x/protocol-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { SamplerCallResult } from '../../types'; +import { SamplerCallResult, SignedNativeOrder } from '../../types'; import { ERC20BridgeSamplerContract } from '../../wrappers'; import { BalancerPoolsCache } from './balancer_utils'; @@ -14,6 +14,7 @@ import { MAINNET_MOONISWAP_V2_1_REGISTRY, MAINNET_MOONISWAP_V2_REGISTRY, MAINNET_SUSHI_SWAP_ROUTER, + MAINNET_UNISWAP_V2_ROUTER, MAX_UINT256, ZERO_AMOUNT, } from './constants'; @@ -93,30 +94,50 @@ export class SamplerOperations { .catch(/* do nothing */); } - public getTokenDecimals(makerTokenAddress: string, takerTokenAddress: string): BatchedOperation { + public getTokenDecimals(tokens: string[]): BatchedOperation { return new SamplerContractOperation({ source: ERC20BridgeSource.Native, contract: this._samplerContract, function: this._samplerContract.getTokenDecimals, - params: [makerTokenAddress, takerTokenAddress], + params: [tokens], }); } - public getOrderFillableTakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation { + public isAddressContract(address: string): BatchedOperation { + return { + encodeCall: () => this._samplerContract.isContract(address).getABIEncodedTransactionData(), + handleCallResults: (callResults: string) => + this._samplerContract.getABIDecodedReturnData('isContract', callResults), + handleRevert: () => { + /* should never happen */ + throw new Error('Invalid address for isAddressContract'); + }, + }; + } + + public getLimitOrderFillableTakerAmounts( + orders: SignedNativeOrder[], + exchangeAddress: string, + ): BatchedOperation { return new SamplerContractOperation({ source: ERC20BridgeSource.Native, contract: this._samplerContract, - function: this._samplerContract.getOrderFillableTakerAssetAmounts, - params: [orders, orders.map(o => o.signature), exchangeAddress], + function: this._samplerContract.getLimitOrderFillableTakerAssetAmounts, + // tslint:disable-next-line:no-unnecessary-type-assertion + params: [orders.map(o => o.order as LimitOrderFields), orders.map(o => o.signature), exchangeAddress], }); } - public getOrderFillableMakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation { + public getLimitOrderFillableMakerAmounts( + orders: SignedNativeOrder[], + exchangeAddress: string, + ): BatchedOperation { return new SamplerContractOperation({ source: ERC20BridgeSource.Native, contract: this._samplerContract, - function: this._samplerContract.getOrderFillableMakerAssetAmounts, - params: [orders, orders.map(o => o.signature), exchangeAddress], + function: this._samplerContract.getLimitOrderFillableMakerAssetAmounts, + // tslint:disable-next-line:no-unnecessary-type-assertion + params: [orders.map(o => o.order as LimitOrderFields), orders.map(o => o.signature), exchangeAddress], }); } @@ -196,10 +217,10 @@ export class SamplerOperations { ): SourceQuoteOperation { return new SamplerContractOperation({ source: ERC20BridgeSource.UniswapV2, - fillData: { tokenAddressPath }, + fillData: { tokenAddressPath, router: MAINNET_UNISWAP_V2_ROUTER }, contract: this._samplerContract, function: this._samplerContract.sampleSellsFromUniswapV2, - params: [tokenAddressPath, takerFillAmounts], + params: [MAINNET_UNISWAP_V2_ROUTER, tokenAddressPath, takerFillAmounts], }); } @@ -209,10 +230,10 @@ export class SamplerOperations { ): SourceQuoteOperation { return new SamplerContractOperation({ source: ERC20BridgeSource.UniswapV2, - fillData: { tokenAddressPath }, + fillData: { tokenAddressPath, router: MAINNET_UNISWAP_V2_ROUTER }, contract: this._samplerContract, function: this._samplerContract.sampleBuysFromUniswapV2, - params: [tokenAddressPath, makerFillAmounts], + params: [MAINNET_UNISWAP_V2_ROUTER, tokenAddressPath, makerFillAmounts], }); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index db719435b8..28ce61153a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -1,12 +1,16 @@ -import { V3RFQIndicativeQuote } from '@0x/quote-server'; -import { MarketOperation, SignedOrder } from '@0x/types'; +import { + FillQuoteTransformerLimitOrderInfo, + FillQuoteTransformerOrderType, + FillQuoteTransformerRfqOrderInfo, +} from '@0x/protocol-utils'; +import { V4RFQIndicativeQuote } from '@0x/quote-server'; +import { MarketOperation } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { RfqtFirmQuoteValidator, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; +import { NativeOrderWithFillableAmounts, RfqtFirmQuoteValidator, RfqtRequestOpts } from '../../types'; import { QuoteRequestor } from '../../utils/quote_requestor'; import { QuoteReport } from '../quote_report_generator'; -import { CollapsedPath } from './path'; import { SourceFilters } from './source_filters'; /** @@ -86,16 +90,18 @@ export interface SnowSwapInfo extends CurveInfo {} // Internal `fillData` field for `Fill` objects. export interface FillData {} -export interface SourceInfo { +// `FillData` for native fills. Represents a single native order +export type NativeRfqOrderFillData = FillQuoteTransformerRfqOrderInfo; +export type NativeLimitOrderFillData = FillQuoteTransformerLimitOrderInfo; +export type NativeFillData = NativeRfqOrderFillData | NativeLimitOrderFillData; + +// Represents an individual DEX sample from the sampler contract +export interface DexSample { source: ERC20BridgeSource; - fillData?: TFillData; + fillData: TFillData; + input: BigNumber; + output: BigNumber; } - -// `FillData` for native fills. -export interface NativeFillData extends FillData { - order: SignedOrderWithFillableAmounts; -} - export interface CurveFillData extends FillData { fromTokenIdx: number; toTokenIdx: number; @@ -120,12 +126,11 @@ export interface BalancerFillData extends FillData { export interface UniswapV2FillData extends FillData { tokenAddressPath: string[]; -} - -export interface SushiSwapFillData extends UniswapV2FillData { router: string; } +export interface SushiSwapFillData extends UniswapV2FillData {} + export interface ShellFillData extends FillData { poolAddress: string; } @@ -153,35 +158,25 @@ export interface DODOFillData extends FillData { poolAddress: string; isSellBase: boolean; } - -export interface Quote { - amount: BigNumber; - fillData?: TFillData; -} - -export interface HopInfo { - sourceIndex: BigNumber; - returnData: string; -} - export interface MultiHopFillData extends FillData { firstHopSource: SourceQuoteOperation; secondHopSource: SourceQuoteOperation; intermediateToken: string; } - -/** - * Represents an individual DEX sample from the sampler contract. - */ -export interface DexSample extends SourceInfo { - input: BigNumber; - output: BigNumber; +export interface HopInfo { + sourceIndex: BigNumber; + returnData: string; } /** * Represents a node on a fill path. */ -export interface Fill extends SourceInfo { +export interface Fill { + // basic data for every fill + source: ERC20BridgeSource; + // TODO jacob people seem to agree that orderType here is more readable + type: FillQuoteTransformerOrderType; // should correspond with TFillData + fillData: TFillData; // Unique ID of the original source path this fill belongs to. // This is generated when the path is generated and is useful to distinguish // paths that have the same `source` IDs but are distinct (e.g., Curves). @@ -203,7 +198,10 @@ export interface Fill extends SourceInfo< /** * Represents continguous fills on a path that have been merged together. */ -export interface CollapsedFill extends SourceInfo { +export interface CollapsedFill { + source: ERC20BridgeSource; + type: FillQuoteTransformerOrderType; // should correspond with TFillData + fillData: TFillData; // Unique ID of the original source path this fill belongs to. // This is generated when the path is generated and is useful to distinguish // paths that have the same `source` IDs but are distinct (e.g., Curves). @@ -230,22 +228,48 @@ export interface CollapsedFill extends So */ export interface NativeCollapsedFill extends CollapsedFill {} +export interface OptimizedMarketOrderBase { + source: ERC20BridgeSource; + fillData: TFillData; + type: FillQuoteTransformerOrderType; // should correspond with TFillData + makerToken: string; + takerToken: string; + makerAmount: BigNumber; // The amount we wish to buy from this order, e.g inclusive of any previous partial fill + takerAmount: BigNumber; // The amount we wish to fill this for, e.g inclusive of any previous partial fill + fills: CollapsedFill[]; +} + +export interface OptimizedMarketBridgeOrder + extends OptimizedMarketOrderBase { + type: FillQuoteTransformerOrderType.Bridge; + fillData: TFillData; + sourcePathId: string; +} + +export interface OptimizedLimitOrder extends OptimizedMarketOrderBase { + type: FillQuoteTransformerOrderType.Limit; + fillData: NativeLimitOrderFillData; +} + +export interface OptimizedRfqOrder extends OptimizedMarketOrderBase { + type: FillQuoteTransformerOrderType.Rfq; + fillData: NativeRfqOrderFillData; +} + /** * Optimized orders to fill. */ -export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts { - /** - * The optimized fills that generated this order. - */ - fills: CollapsedFill[]; -} +export type OptimizedMarketOrder = + | OptimizedMarketBridgeOrder + | OptimizedMarketOrderBase + | OptimizedMarketOrderBase; export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts { quoteRequestor?: QuoteRequestor; firmQuoteValidator?: RfqtFirmQuoteValidator; } -export type FeeEstimate = (fillData?: FillData) => number | BigNumber; +export type FeeEstimate = (fillData: FillData) => number | BigNumber; export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>; export type ExchangeProxyOverhead = (sourceFlags: number) => BigNumber; @@ -313,6 +337,9 @@ export interface GetMarketOrdersOpts { * sources. Defaults to `true`. */ allowFallback: boolean; + /** + * Options for RFQT such as takerAddress, intent on filling + */ rfqt?: GetMarketOrdersRfqtOpts; /** * Whether to generate a quote report @@ -334,10 +361,9 @@ export interface BatchedOperation { handleRevert(callResults: string): TResult; } -export interface SourceQuoteOperation - extends BatchedOperation, - SourceInfo { +export interface SourceQuoteOperation extends BatchedOperation { readonly source: ERC20BridgeSource; + fillData: TFillData; } export interface OptimizerResult { @@ -346,9 +372,8 @@ export interface OptimizerResult { liquidityDelivered: CollapsedFill[] | DexSample; marketSideLiquidity: MarketSideLiquidity; adjustedRate: BigNumber; - unoptimizedPath?: CollapsedPath; - takerAssetToEthRate: BigNumber; - makerAssetToEthRate: BigNumber; + takerTokenToEthRate: BigNumber; + makerTokenToEthRate: BigNumber; } export interface OptimizerResultWithReport extends OptimizerResult { @@ -369,16 +394,20 @@ export interface MarketSideLiquidity { inputAmount: BigNumber; inputToken: string; outputToken: string; - dexQuotes: Array>>; - nativeOrders: SignedOrder[]; - orderFillableAmounts: BigNumber[]; ethToOutputRate: BigNumber; ethToInputRate: BigNumber; - rfqtIndicativeQuotes: V3RFQIndicativeQuote[]; - twoHopQuotes: Array>; quoteSourceFilters: SourceFilters; makerTokenDecimals: number; takerTokenDecimals: number; + quotes: RawQuotes; + isRfqSupported: boolean; +} + +export interface RawQuotes { + nativeOrders: NativeOrderWithFillableAmounts[]; + rfqtIndicativeQuotes: V4RFQIndicativeQuote[]; + twoHopQuotes: Array>; + dexQuotes: Array>>; } export interface TokenAdjacencyGraph { diff --git a/packages/asset-swapper/src/utils/order_prune_utils.ts b/packages/asset-swapper/src/utils/order_prune_utils.ts deleted file mode 100644 index 35b7a72928..0000000000 --- a/packages/asset-swapper/src/utils/order_prune_utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { orderCalculationUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; -import * as _ from 'lodash'; - -import { constants } from '../constants'; -import { OrderPrunerPermittedFeeTypes } from '../types'; -import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from '../utils/utils'; - -export const orderPrunerUtils = { - pruneForUsableSignedOrders( - signedOrders: SignedOrder[], - permittedOrderFeeTypes: Set, - expiryBufferMs: number, - ): SignedOrder[] { - const result = _.filter(signedOrders, order => { - return ( - orderCalculationUtils.isOpenOrder(order) && - !orderCalculationUtils.willOrderExpire(order, expiryBufferMs / constants.ONE_SECOND_MS) && - ((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) && - order.takerFee.eq(constants.ZERO_AMOUNT)) || - (permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) && - isOrderTakerFeePayableWithTakerAsset(order)) || - (permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) && - isOrderTakerFeePayableWithMakerAsset(order))) - ); - }); - return result; - }, -}; diff --git a/packages/asset-swapper/src/utils/order_state_utils.ts b/packages/asset-swapper/src/utils/order_state_utils.ts deleted file mode 100644 index e512db2bde..0000000000 --- a/packages/asset-swapper/src/utils/order_state_utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DevUtilsContract } from '@0x/contract-wrappers'; -import { orderCalculationUtils } from '@0x/order-utils'; -import { OrderStatus, SignedOrder } from '@0x/types'; - -import { constants } from '../constants'; -import { OrderPrunerOnChainMetadata, SignedOrderWithFillableAmounts } from '../types'; - -/** - * Utility class to retrieve order state if needed outside of using the ERC20BridgeSampler - */ -export class OrderStateUtils { - private readonly _devUtils: DevUtilsContract; - - constructor(devUtils: DevUtilsContract) { - this._devUtils = devUtils; - } - - public async getSignedOrdersWithFillableAmountsAsync( - signedOrders: SignedOrder[], - ): Promise { - const signatures = signedOrders.map(o => o.signature); - const [ordersInfo, fillableTakerAssetAmounts, isValidSignatures] = await this._devUtils - .getOrderRelevantStates(signedOrders, signatures) - .callAsync(); - const ordersOnChainMetadata: OrderPrunerOnChainMetadata[] = ordersInfo.map((orderInfo, index) => { - return { - ...orderInfo, - fillableTakerAssetAmount: fillableTakerAssetAmounts[index], - isValidSignature: isValidSignatures[index], - }; - }); - // take orders + on chain information and find the valid orders and fillable makerAsset or takerAsset amounts - return signedOrders.map( - (order: SignedOrder, index: number): SignedOrderWithFillableAmounts => { - const orderMetadata = ordersOnChainMetadata[index]; - const fillableTakerAssetAmount = - orderMetadata.isValidSignature && orderMetadata.orderStatus === OrderStatus.Fillable - ? orderMetadata.fillableTakerAssetAmount - : constants.ZERO_AMOUNT; - return { - ...order, - fillableTakerAssetAmount, - fillableMakerAssetAmount: orderCalculationUtils.getMakerFillAmount(order, fillableTakerAssetAmount), - fillableTakerFeeAmount: orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount), - }; - }, - ); - } -} diff --git a/packages/asset-swapper/src/utils/quote_report_generator.ts b/packages/asset-swapper/src/utils/quote_report_generator.ts index 088b941286..44b7532cc4 100644 --- a/packages/asset-swapper/src/utils/quote_report_generator.ts +++ b/packages/asset-swapper/src/utils/quote_report_generator.ts @@ -1,7 +1,8 @@ -import { SignedOrder } from '@0x/types'; +import { FillQuoteTransformerOrderType, Signature } from '@0x/protocol-utils'; import { BigNumber } from '@0x/utils'; +import _ = require('lodash'); -import { MarketOperation } from '../types'; +import { MarketOperation, NativeOrderWithFillableAmounts } from '../types'; import { CollapsedFill, @@ -10,48 +11,52 @@ import { FillData, MultiHopFillData, NativeCollapsedFill, + NativeFillData, + NativeLimitOrderFillData, + NativeRfqOrderFillData, } from './market_operation_utils/types'; import { QuoteRequestor } from './quote_requestor'; -export interface BridgeReportSource { - liquiditySource: Exclude; +export interface QuoteReportEntryBase { + liquiditySource: ERC20BridgeSource; makerAmount: BigNumber; takerAmount: BigNumber; - fillData?: FillData; -} - -export interface MultiHopReportSource { - liquiditySource: ERC20BridgeSource.MultiHop; - makerAmount: BigNumber; - takerAmount: BigNumber; - hopSources: ERC20BridgeSource[]; fillData: FillData; } - -interface NativeReportSourceBase { - liquiditySource: ERC20BridgeSource.Native; - makerAmount: BigNumber; - takerAmount: BigNumber; - nativeOrder: SignedOrder; - fillableTakerAmount: BigNumber; +export interface BridgeQuoteReportEntry extends QuoteReportEntryBase { + liquiditySource: Exclude; } -export interface NativeOrderbookReportSource extends NativeReportSourceBase { + +export interface MultiHopQuoteReportEntry extends QuoteReportEntryBase { + liquiditySource: ERC20BridgeSource.MultiHop; + hopSources: ERC20BridgeSource[]; +} + +export interface NativeLimitOrderQuoteReportEntry extends QuoteReportEntryBase { + liquiditySource: ERC20BridgeSource.Native; + fillData: NativeFillData; + fillableTakerAmount: BigNumber; isRfqt: false; } -export interface NativeRFQTReportSource extends NativeReportSourceBase { + +export interface NativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase { + liquiditySource: ERC20BridgeSource.Native; + fillData: NativeFillData; + fillableTakerAmount: BigNumber; isRfqt: true; makerUri: string; comparisonPrice?: number; } -export type QuoteReportSource = - | BridgeReportSource - | NativeOrderbookReportSource - | NativeRFQTReportSource - | MultiHopReportSource; + +export type QuoteReportEntry = + | BridgeQuoteReportEntry + | MultiHopQuoteReportEntry + | NativeLimitOrderQuoteReportEntry + | NativeRfqOrderQuoteReportEntry; export interface QuoteReport { - sourcesConsidered: QuoteReportSource[]; - sourcesDelivered: QuoteReportSource[]; + sourcesConsidered: QuoteReportEntry[]; + sourcesDelivered: QuoteReportEntry[]; } /** @@ -62,15 +67,14 @@ export function generateQuoteReport( marketOperation: MarketOperation, dexQuotes: DexSample[], multiHopQuotes: Array>, - nativeOrders: SignedOrder[], - orderFillableAmounts: BigNumber[], + nativeOrders: NativeOrderWithFillableAmounts[], liquidityDelivered: ReadonlyArray | DexSample, comparisonPrice?: BigNumber | undefined, quoteRequestor?: QuoteRequestor, ): QuoteReport { const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation)); - const nativeOrderSourcesConsidered = nativeOrders.map((order, idx) => - _nativeOrderToReportSource(order, orderFillableAmounts[idx], comparisonPrice, quoteRequestor), + const nativeOrderSourcesConsidered = nativeOrders.map(order => + _nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor), ); const multiHopSourcesConsidered = multiHopQuotes.map(quote => _multiHopSampleToReportSource(quote, marketOperation), @@ -84,17 +88,18 @@ export function generateQuoteReport( let sourcesDelivered; if (Array.isArray(liquidityDelivered)) { // create easy way to look up fillable amounts - const nativeOrderSignaturesToFillableAmounts = _nativeOrderSignaturesToFillableAmounts( - nativeOrders, - orderFillableAmounts, + const nativeOrderSignaturesToFillableAmounts = _.fromPairs( + nativeOrders.map(o => { + return [_nativeDataToId(o), o.fillableTakerAmount]; + }), ); // map sources delivered sourcesDelivered = liquidityDelivered.map(collapsedFill => { - const foundNativeOrder = _nativeOrderFromCollapsedFill(collapsedFill); - if (foundNativeOrder) { - return _nativeOrderToReportSource( - foundNativeOrder, - nativeOrderSignaturesToFillableAmounts[foundNativeOrder.signature], + if (_isNativeOrderFromCollapsedFill(collapsedFill)) { + return _nativeOrderToReportEntry( + collapsedFill.type, + collapsedFill.fillData, + nativeOrderSignaturesToFillableAmounts[_nativeDataToId(collapsedFill.fillData)], comparisonPrice, quoteRequestor, ); @@ -104,6 +109,7 @@ export function generateQuoteReport( }); } else { sourcesDelivered = [ + // tslint:disable-next-line: no-unnecessary-type-assertion _multiHopSampleToReportSource(liquidityDelivered as DexSample, marketOperation), ]; } @@ -113,7 +119,12 @@ export function generateQuoteReport( }; } -function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeReportSource { +function _nativeDataToId(data: { signature: Signature }): string { + const { v, r, s } = data.signature; + return `${v}${r}${s}`; +} + +function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeQuoteReportEntry { const liquiditySource = ds.source; if (liquiditySource === ERC20BridgeSource.Native) { @@ -144,8 +155,8 @@ function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperatio function _multiHopSampleToReportSource( ds: DexSample, marketOperation: MarketOperation, -): MultiHopReportSource { - const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData!; +): MultiHopQuoteReportEntry { + const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData; // input and output map to different values // based on the market operation if (marketOperation === MarketOperation.Buy) { @@ -153,7 +164,7 @@ function _multiHopSampleToReportSource( liquiditySource: ERC20BridgeSource.MultiHop, makerAmount: ds.input, takerAmount: ds.output, - fillData: ds.fillData!, + fillData: ds.fillData, hopSources: [firstHop.source, secondHop.source], }; } else if (marketOperation === MarketOperation.Sell) { @@ -161,7 +172,7 @@ function _multiHopSampleToReportSource( liquiditySource: ERC20BridgeSource.MultiHop, makerAmount: ds.output, takerAmount: ds.input, - fillData: ds.fillData!, + fillData: ds.fillData, hopSources: [firstHop.source, secondHop.source], }; } else { @@ -169,65 +180,44 @@ function _multiHopSampleToReportSource( } } -function _nativeOrderSignaturesToFillableAmounts( - nativeOrders: SignedOrder[], - fillableAmounts: BigNumber[], -): { [orderSignature: string]: BigNumber } { - // create easy way to look up fillable amounts based on native order signatures - if (fillableAmounts.length !== nativeOrders.length) { - // length mismatch, abort - throw new Error('orderFillableAmounts must be the same length as nativeOrders'); - } - const nativeOrderSignaturesToFillableAmounts: { [orderSignature: string]: BigNumber } = {}; - nativeOrders.forEach((nativeOrder, idx) => { - nativeOrderSignaturesToFillableAmounts[nativeOrder.signature] = fillableAmounts[idx]; - }); - return nativeOrderSignaturesToFillableAmounts; +function _isNativeOrderFromCollapsedFill(cf: CollapsedFill): cf is NativeCollapsedFill { + const { type } = cf; + return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq; } -function _nativeOrderFromCollapsedFill(cf: CollapsedFill): SignedOrder | undefined { - // Cast as NativeCollapsedFill and then check - // if it really is a NativeCollapsedFill - const possibleNativeCollapsedFill = cf as NativeCollapsedFill; - if (possibleNativeCollapsedFill.fillData && possibleNativeCollapsedFill.fillData.order) { - return possibleNativeCollapsedFill.fillData.order; - } else { - return undefined; - } -} - -function _nativeOrderToReportSource( - nativeOrder: SignedOrder, +function _nativeOrderToReportEntry( + type: FillQuoteTransformerOrderType, + fillData: NativeLimitOrderFillData | NativeRfqOrderFillData, fillableAmount: BigNumber, comparisonPrice?: BigNumber | undefined, quoteRequestor?: QuoteRequestor, -): NativeRFQTReportSource | NativeOrderbookReportSource { - const nativeOrderBase: NativeReportSourceBase = { +): NativeRfqOrderQuoteReportEntry | NativeLimitOrderQuoteReportEntry { + const nativeOrderBase = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: nativeOrder.makerAssetAmount, - takerAmount: nativeOrder.takerAssetAmount, + makerAmount: fillData.order.makerAmount, + takerAmount: fillData.order.takerAmount, fillableTakerAmount: fillableAmount, - nativeOrder, }; // if we find this is an rfqt order, label it as such and associate makerUri - const foundRfqtMakerUri = quoteRequestor && quoteRequestor.getMakerUriForOrderSignature(nativeOrder.signature); - if (foundRfqtMakerUri) { - const rfqtSource: NativeRFQTReportSource = { + const isRfqt = type === FillQuoteTransformerOrderType.Rfq; + const rfqtMakerUri = isRfqt ? quoteRequestor!.getMakerUriForSignature(fillData.signature) : undefined; + + if (isRfqt) { + // tslint:disable-next-line: no-object-literal-type-assertion + return { ...nativeOrderBase, isRfqt: true, - makerUri: foundRfqtMakerUri, - }; - if (comparisonPrice) { - rfqtSource.comparisonPrice = comparisonPrice.toNumber(); - } - return rfqtSource; + makerUri: rfqtMakerUri || '', + ...(comparisonPrice ? { comparisonPrice: comparisonPrice.toNumber() } : {}), + fillData, + } as NativeRfqOrderQuoteReportEntry; } else { - // if it's not an rfqt order, treat as normal - const regularNativeOrder: NativeOrderbookReportSource = { + // tslint:disable-next-line: no-object-literal-type-assertion + return { ...nativeOrderBase, isRfqt: false, - }; - return regularNativeOrder; + fillData, + } as NativeLimitOrderQuoteReportEntry; } } diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index c4016cdcbf..3f158c1ef6 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -1,14 +1,13 @@ import { schemas, SchemaValidator } from '@0x/json-schemas'; -import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; -import { TakerRequestQueryParams, V3RFQFirmQuote, V3RFQIndicativeQuote } from '@0x/quote-server'; -import { ERC20AssetData } from '@0x/types'; -import { BigNumber } from '@0x/utils'; +import { FillQuoteTransformerOrderType, Signature } from '@0x/protocol-utils'; +import { TakerRequestQueryParams, V4RFQFirmQuote, V4RFQIndicativeQuote, V4SignedRfqOrder } from '@0x/quote-server'; +import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import Axios, { AxiosInstance } from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; import { constants } from '../constants'; -import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; +import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts, SignedNativeOrder } from '../types'; import { ONE_SECOND_MS } from './market_operation_utils/constants'; import { RfqMakerBlacklist } from './rfq_maker_blacklist'; @@ -25,34 +24,17 @@ const MAKER_TIMEOUT_STREAK_LENGTH = 10; const MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES = 10; const rfqMakerBlacklist = new RfqMakerBlacklist(MAKER_TIMEOUT_STREAK_LENGTH, MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES); +interface RfqQuote { + response: T; + makerUri: string; +} + /** * Request quotes from RFQ-T providers */ -function getTokenAddressOrThrow(assetData: string): string { - const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); - if (decodedAssetData.hasOwnProperty('tokenAddress')) { - // type cast necessary here as decodeAssetDataOrThrow returns - // an AssetData object, which doesn't necessarily contain a - // token address. (it could possibly be a StaticCallAssetData, - // which lacks an address.) so we'll just assume it's a token - // here. should be safe, with the enclosing guard condition - // and subsequent error. - // tslint:disable-next-line:no-unnecessary-type-assertion - return (decodedAssetData as ERC20AssetData).tokenAddress; - } - throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`); -} - -function hasExpectedAssetData( - expectedMakerAssetData: string, - expectedTakerAssetData: string, - makerAssetDataInQuestion: string, - takerAssetDataInQuestion: string, -): boolean { - const hasExpectedMakerAssetData = makerAssetDataInQuestion.toLowerCase() === expectedMakerAssetData.toLowerCase(); - const hasExpectedTakerAssetData = takerAssetDataInQuestion.toLowerCase() === expectedTakerAssetData.toLowerCase(); - return hasExpectedMakerAssetData && hasExpectedTakerAssetData; +function hasExpectedAddresses(comparisons: Array<[string, string]>): boolean { + return comparisons.every(c => c[0].toLowerCase() === c[1].toLowerCase()); } function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has AxiosError.toJSON() returning object */ { @@ -84,20 +66,24 @@ function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has Axi } } +function nativeDataToId(data: { signature: Signature }): string { + const { v, r, s } = data.signature; + return `${v}${r}${s}`; +} + export class QuoteRequestor { private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); - private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {}; + private readonly _orderSignatureToMakerUri: { [signature: string]: string } = {}; public static makeQueryParameters( + txOrigin: string, takerAddress: string, marketOperation: MarketOperation, - makerAssetData: string, - takerAssetData: string, + buyTokenAddress: string, // maker token + sellTokenAddress: string, // taker token assetFillAmount: BigNumber, comparisonPrice?: BigNumber, ): TakerRequestQueryParams { - const buyTokenAddress = getTokenAddressOrThrow(makerAssetData); - const sellTokenAddress = getTokenAddressOrThrow(takerAssetData); const { buyAmountBaseUnits, sellAmountBaseUnits } = marketOperation === MarketOperation.Buy ? { @@ -111,15 +97,14 @@ export class QuoteRequestor { const requestParamsWithBigNumbers: Pick< TakerRequestQueryParams, - 'buyTokenAddress' | 'sellTokenAddress' | 'takerAddress' | 'comparisonPrice' | 'protocolVersion' + 'buyTokenAddress' | 'sellTokenAddress' | 'txOrigin' | 'comparisonPrice' | 'protocolVersion' | 'takerAddress' > = { + txOrigin, takerAddress, comparisonPrice: comparisonPrice === undefined ? undefined : comparisonPrice.toString(), buyTokenAddress, sellTokenAddress, - - // The request parameter below defines what protocol version the RFQ servers should be returning. - protocolVersion: '3', + protocolVersion: '4', }; // convert BigNumbers to strings @@ -149,105 +134,95 @@ export class QuoteRequestor { } public async requestRfqtFirmQuotesAsync( - makerAssetData: string, - takerAssetData: string, + makerToken: string, // maker token + takerToken: string, // taker token assetFillAmount: BigNumber, marketOperation: MarketOperation, comparisonPrice: BigNumber | undefined, options: RfqtRequestOpts, - ): Promise { + ): Promise { const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; - if ( - _opts.takerAddress === undefined || - _opts.takerAddress === '' || - _opts.takerAddress === '0x' || - !_opts.takerAddress || - _opts.takerAddress === constants.NULL_ADDRESS - ) { - throw new Error('RFQ-T firm quotes require the presence of a taker address'); + if (!_opts.txOrigin || [undefined, '', '0x', NULL_ADDRESS].includes(_opts.txOrigin)) { + throw new Error('RFQ-T firm quotes require the presence of a tx origin'); } - const firmQuoteResponses = await this._getQuotesAsync( // not yet BigNumber - makerAssetData, - takerAssetData, + const quotesRaw = await this._getQuotesAsync( + makerToken, + takerToken, assetFillAmount, marketOperation, comparisonPrice, _opts, 'firm', ); + const quotes = quotesRaw.map(result => ({ ...result, response: result.response.signedOrder })); - const result: V3RFQFirmQuote[] = []; - firmQuoteResponses.forEach(firmQuoteResponse => { - const orderWithStringInts = firmQuoteResponse.response.signedOrder; - + // validate + const validationFunction = (o: V4SignedRfqOrder) => { try { - const hasValidSchema = this._schemaValidator.isValid(orderWithStringInts, schemas.signedOrderSchema); - if (!hasValidSchema) { - throw new Error('Order not valid'); - } - } catch (err) { - this._warningLogger(orderWithStringInts, `Invalid RFQ-t order received, filtering out. ${err.message}`); - return; + // Handle the validate throwing, i.e if it isn't an object or json response + return this._schemaValidator.isValid(o, schemas.v4RfqSignedOrderSchema); + } catch (e) { + return false; + } + }; + const validQuotes = quotes.filter(result => { + const order = result.response; + if (!validationFunction(order)) { + this._warningLogger(result, 'Invalid RFQ-T firm quote received, filtering out'); + return false; } - if ( - !hasExpectedAssetData( - makerAssetData, - takerAssetData, - orderWithStringInts.makerAssetData.toLowerCase(), - orderWithStringInts.takerAssetData.toLowerCase(), - ) + !hasExpectedAddresses([ + [makerToken, order.makerToken], + [takerToken, order.takerToken], + [_opts.takerAddress, order.taker], + [_opts.txOrigin, order.txOrigin], + ]) ) { - this._warningLogger(orderWithStringInts, 'Unexpected asset data in RFQ-T order, filtering out'); - return; + this._warningLogger( + order, + 'Unexpected token, tx origin or taker address in RFQ-T order, filtering out', + ); + return false; } - - if (orderWithStringInts.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) { - this._warningLogger(orderWithStringInts, 'Unexpected takerAddress in RFQ-T order, filtering out'); - return; + if (this._isExpirationTooSoon(new BigNumber(order.expiry))) { + this._warningLogger(order, 'Expiry too soon in RFQ-T firm quote, filtering out'); + return false; + } else { + return true; } - - const orderWithBigNumberInts: SignedOrder = { - ...orderWithStringInts, - makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount), - takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount), - makerFee: new BigNumber(orderWithStringInts.makerFee), - takerFee: new BigNumber(orderWithStringInts.takerFee), - expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds), - salt: new BigNumber(orderWithStringInts.salt), - }; - - if ( - orderCalculationUtils.willOrderExpire( - orderWithBigNumberInts, - this._expiryBufferMs / constants.ONE_SECOND_MS, - ) - ) { - this._warningLogger(orderWithBigNumberInts, 'Expiry too soon in RFQ-T order, filtering out'); - return; - } - - // Store makerUri for looking up later - this._orderSignatureToMakerUri[orderWithBigNumberInts.signature] = firmQuoteResponse.makerUri; - - // Passed all validation, add it to result - result.push({ signedOrder: orderWithBigNumberInts }); - return; }); - return result; + + // Save the maker URI for later and return just the order + const rfqQuotes = validQuotes.map(result => { + const { signature, ...rest } = result.response; + const order: SignedNativeOrder = { + order: { + ...rest, + makerAmount: new BigNumber(result.response.makerAmount), + takerAmount: new BigNumber(result.response.takerAmount), + expiry: new BigNumber(result.response.expiry), + salt: new BigNumber(result.response.salt), + }, + type: FillQuoteTransformerOrderType.Rfq, + signature, + }; + this._orderSignatureToMakerUri[nativeDataToId(result.response)] = result.makerUri; + return order; + }); + return rfqQuotes; } public async requestRfqtIndicativeQuotesAsync( - makerAssetData: string, - takerAssetData: string, + makerToken: string, + takerToken: string, assetFillAmount: BigNumber, marketOperation: MarketOperation, comparisonPrice: BigNumber | undefined, options: RfqtRequestOpts, - ): Promise { + ): Promise { const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; - // Originally a takerAddress was required for indicative quotes, but // now we've eliminated that requirement. @0x/quote-server, however, // is still coded to expect a takerAddress. So if the client didn't @@ -256,10 +231,12 @@ export class QuoteRequestor { if (!_opts.takerAddress) { _opts.takerAddress = constants.NULL_ADDRESS; } - - const responsesWithStringInts = await this._getQuotesAsync( // not yet BigNumber - makerAssetData, - takerAssetData, + if (!_opts.txOrigin) { + _opts.txOrigin = constants.NULL_ADDRESS; + } + const rawQuotes = await this._getQuotesAsync( + makerToken, + takerToken, assetFillAmount, marketOperation, comparisonPrice, @@ -267,84 +244,78 @@ export class QuoteRequestor { 'indicative', ); - const validResponsesWithStringInts = responsesWithStringInts.filter(result => { - const response = result.response; - if (!this._isValidRfqtIndicativeQuoteResponse(response)) { - this._warningLogger(response, 'Invalid RFQ-T indicative quote received, filtering out'); + // validate + const validationFunction = (o: V4RFQIndicativeQuote) => this._isValidRfqtIndicativeQuoteResponse(o); + const validQuotes = rawQuotes.filter(result => { + const order = result.response; + if (!validationFunction(order)) { + this._warningLogger(result, 'Invalid RFQ-T indicative quote received, filtering out'); return false; } - if ( - !hasExpectedAssetData(makerAssetData, takerAssetData, response.makerAssetData, response.takerAssetData) - ) { - this._warningLogger(response, 'Unexpected asset data in RFQ-T indicative quote, filtering out'); + if (!hasExpectedAddresses([[makerToken, order.makerToken], [takerToken, order.takerToken]])) { + this._warningLogger(order, 'Unexpected token or taker address in RFQ-T order, filtering out'); return false; } - return true; - }); - - const validResponses = validResponsesWithStringInts.map(result => { - const response = result.response; - return { - ...response, - makerAssetAmount: new BigNumber(response.makerAssetAmount), - takerAssetAmount: new BigNumber(response.takerAssetAmount), - expirationTimeSeconds: new BigNumber(response.expirationTimeSeconds), - }; - }); - - const responses = validResponses.filter(response => { - if (this._isExpirationTooSoon(response.expirationTimeSeconds)) { - this._warningLogger(response, 'Expiry too soon in RFQ-T indicative quote, filtering out'); + if (this._isExpirationTooSoon(new BigNumber(order.expiry))) { + this._warningLogger(order, 'Expiry too soon in RFQ-T indicative quote, filtering out'); return false; + } else { + return true; } - return true; }); - - return responses; + const quotes = validQuotes.map(r => r.response); + quotes.forEach(q => { + q.makerAmount = new BigNumber(q.makerAmount); + q.takerAmount = new BigNumber(q.takerAmount); + q.expiry = new BigNumber(q.expiry); + }); + return quotes; } /** * Given an order signature, returns the makerUri that the order originated from */ - public getMakerUriForOrderSignature(orderSignature: string): string | undefined { - return this._orderSignatureToMakerUri[orderSignature]; + public getMakerUriForSignature(signature: Signature): string | undefined { + return this._orderSignatureToMakerUri[nativeDataToId({ signature })]; } - private _isValidRfqtIndicativeQuoteResponse(response: V3RFQIndicativeQuote): boolean { - const hasValidMakerAssetAmount = - response.makerAssetAmount !== undefined && - this._schemaValidator.isValid(response.makerAssetAmount, schemas.wholeNumberSchema); - const hasValidTakerAssetAmount = - response.takerAssetAmount !== undefined && - this._schemaValidator.isValid(response.takerAssetAmount, schemas.wholeNumberSchema); - const hasValidMakerAssetData = - response.makerAssetData !== undefined && - this._schemaValidator.isValid(response.makerAssetData, schemas.hexSchema); - const hasValidTakerAssetData = - response.takerAssetData !== undefined && - this._schemaValidator.isValid(response.takerAssetData, schemas.hexSchema); - const hasValidExpirationTimeSeconds = - response.expirationTimeSeconds !== undefined && - this._schemaValidator.isValid(response.expirationTimeSeconds, schemas.wholeNumberSchema); - if ( - hasValidMakerAssetAmount && - hasValidTakerAssetAmount && - hasValidMakerAssetData && - hasValidTakerAssetData && - hasValidExpirationTimeSeconds - ) { - return true; + private _isValidRfqtIndicativeQuoteResponse(response: V4RFQIndicativeQuote): boolean { + const requiredKeys: Array = [ + 'makerAmount', + 'takerAmount', + 'makerToken', + 'takerToken', + 'expiry', + ]; + + for (const k of requiredKeys) { + if (response[k] === undefined) { + return false; + } } - return false; + // TODO (jacob): I have a feeling checking 5 schemas is slower then checking one + const hasValidMakerAssetAmount = this._schemaValidator.isValid(response.makerAmount, schemas.wholeNumberSchema); + const hasValidTakerAssetAmount = this._schemaValidator.isValid(response.takerAmount, schemas.wholeNumberSchema); + const hasValidMakerToken = this._schemaValidator.isValid(response.makerToken, schemas.hexSchema); + const hasValidTakerToken = this._schemaValidator.isValid(response.takerToken, schemas.hexSchema); + const hasValidExpirationTimeSeconds = this._schemaValidator.isValid(response.expiry, schemas.wholeNumberSchema); + if ( + !hasValidMakerAssetAmount || + !hasValidTakerAssetAmount || + !hasValidMakerToken || + !hasValidTakerToken || + !hasValidExpirationTimeSeconds + ) { + return false; + } + return true; } - private _makerSupportsPair(makerUrl: string, makerAssetData: string, takerAssetData: string): boolean { - const makerTokenAddress = getTokenAddressOrThrow(makerAssetData); - const takerTokenAddress = getTokenAddressOrThrow(takerAssetData); + private _makerSupportsPair(makerUrl: string, makerToken: string, takerToken: string): boolean { for (const assetPair of this._rfqtAssetOfferings[makerUrl]) { if ( - (assetPair[0] === makerTokenAddress && assetPair[1] === takerTokenAddress) || - (assetPair[0] === takerTokenAddress && assetPair[1] === makerTokenAddress) + (assetPair[0] === makerToken && assetPair[1] === takerToken) || + (assetPair[0] === takerToken && assetPair[1] === makerToken) ) { return true; } @@ -359,92 +330,101 @@ export class QuoteRequestor { } private async _getQuotesAsync( - makerAssetData: string, - takerAssetData: string, + makerToken: string, + takerToken: string, assetFillAmount: BigNumber, marketOperation: MarketOperation, comparisonPrice: BigNumber | undefined, options: RfqtRequestOpts, quoteType: 'firm' | 'indicative', - ): Promise> { + ): Promise>> { const requestParams = QuoteRequestor.makeQueryParameters( + options.txOrigin, options.takerAddress, marketOperation, - makerAssetData, - takerAssetData, + makerToken, + takerToken, assetFillAmount, comparisonPrice, ); + const quotePath = (() => { + switch (quoteType) { + case 'firm': + return 'quote'; + case 'indicative': + return 'price'; + default: + throw new Error(`Unexpected quote type ${quoteType}`); + } + })(); - const result: Array<{ response: ResponseT; makerUri: string }> = []; - await Promise.all( - Object.keys(this._rfqtAssetOfferings).map(async url => { - const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url); - const partialLogEntry = { url, quoteType, requestParams, isBlacklisted }; - if (isBlacklisted) { - this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } }); - } else if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { - const timeBeforeAwait = Date.now(); - const maxResponseTimeMs = - options.makerEndpointMaxResponseTimeMs === undefined - ? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs! - : options.makerEndpointMaxResponseTimeMs; - try { - const quotePath = (() => { - switch (quoteType) { - case 'firm': - return 'quote'; - case 'indicative': - return 'price'; - default: - throw new Error(`Unexpected quote type ${quoteType}`); - } - })(); - const response = await quoteRequestorHttpClient.get(`${url}/${quotePath}`, { - headers: { '0x-api-key': options.apiKey }, - params: requestParams, - timeout: maxResponseTimeMs, - }); - const latencyMs = Date.now() - timeBeforeAwait; - this._infoLogger({ - rfqtMakerInteraction: { - ...partialLogEntry, - response: { - included: true, - apiKey: options.apiKey, - takerAddress: requestParams.takerAddress, - statusCode: response.status, - latencyMs, - }, + const makerUrls = Object.keys(this._rfqtAssetOfferings); + const quotePromises = makerUrls.map(async url => { + // filter out requests to skip + const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url); + const partialLogEntry = { url, quoteType, requestParams, isBlacklisted }; + if (isBlacklisted) { + this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } }); + return; + } else if (!this._makerSupportsPair(url, makerToken, takerToken)) { + return; + } else { + // make request to MMs + const timeBeforeAwait = Date.now(); + const maxResponseTimeMs = + options.makerEndpointMaxResponseTimeMs === undefined + ? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs! + : options.makerEndpointMaxResponseTimeMs; + try { + const response = await quoteRequestorHttpClient.get(`${url}/${quotePath}`, { + headers: { '0x-api-key': options.apiKey }, + params: requestParams, + timeout: maxResponseTimeMs, + }); + const latencyMs = Date.now() - timeBeforeAwait; + this._infoLogger({ + rfqtMakerInteraction: { + ...partialLogEntry, + response: { + included: true, + apiKey: options.apiKey, + takerAddress: requestParams.takerAddress, + txOrigin: requestParams.txOrigin, + statusCode: response.status, + latencyMs, }, - }); - rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); - result.push({ response: response.data, makerUri: url }); - } catch (err) { - const latencyMs = Date.now() - timeBeforeAwait; - this._infoLogger({ - rfqtMakerInteraction: { - ...partialLogEntry, - response: { - included: false, - apiKey: options.apiKey, - takerAddress: requestParams.takerAddress, - statusCode: err.response ? err.response.status : undefined, - latencyMs, - }, + }, + }); + rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); + return { response: response.data, makerUri: url }; + } catch (err) { + // log error if any + const latencyMs = Date.now() - timeBeforeAwait; + this._infoLogger({ + rfqtMakerInteraction: { + ...partialLogEntry, + response: { + included: false, + apiKey: options.apiKey, + takerAddress: requestParams.takerAddress, + txOrigin: requestParams.txOrigin, + statusCode: err.response ? err.response.status : undefined, + latencyMs, }, - }); - rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); - this._warningLogger( - convertIfAxiosError(err), - `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ - options.apiKey - } for taker address ${options.takerAddress}`, - ); - } + }, + }); + rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); + this._warningLogger( + convertIfAxiosError(err), + `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ + options.apiKey + } for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`, + ); + return; } - }), - ); - return result; + } + }); + const results = (await Promise.all(quotePromises)).filter(x => x !== undefined); + return results as Array>; } } diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index 863abb1489..2eb1851b4b 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -1,10 +1,11 @@ +import { FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { BigNumber } from '@0x/utils'; import { constants } from '../constants'; import { MarketOperation } from '../types'; -import { CollapsedFill, ERC20BridgeSource, FeeSchedule, OptimizedMarketOrder } from './market_operation_utils/types'; -import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; +import { FeeSchedule, NativeLimitOrderFillData, OptimizedMarketOrder } from './market_operation_utils/types'; +import { getNativeAdjustedTakerFeeAmount } from './utils'; const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; const { ROUND_DOWN, ROUND_UP } = BigNumber; @@ -73,11 +74,13 @@ export interface QuoteFillInfo { export interface QuoteFillInfoOpts { gasSchedule: FeeSchedule; protocolFeeMultiplier: BigNumber; + slippage: number; } const DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS: QuoteFillInfoOpts = { gasSchedule: {}, protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER, + slippage: 0, }; export interface QuoteFillOrderCall { @@ -117,17 +120,20 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult ...quoteInfo.opts, }; const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier); + const bestCase = createBestCaseFillOrderCalls(quoteInfo); const result = { - ...fillQuoteOrders( - createWorstCaseFillOrderCalls(quoteInfo), - quoteInfo.fillAmount, - protocolFeePerFillOrder, - opts.gasSchedule, - ), + ...fillQuoteOrders(bestCase, quoteInfo.fillAmount, protocolFeePerFillOrder, opts.gasSchedule), // Worst case gas and protocol fee is hitting all orders. - gas: getTotalGasUsedByFills(getFlattenedFillsFromOrders(quoteInfo.orders), opts.gasSchedule), - protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length), + gas: getTotalGasUsedByFills(quoteInfo.orders, opts.gasSchedule), + protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.filter(o => hasProtocolFee(o)).length), }; + // Adjust the output by 1-slippage for the worst case if it is a sell + // Adjust the output by 1+slippage for the worst case if it is a buy + const outputMultiplier = + quoteInfo.side === MarketOperation.Sell + ? new BigNumber(1).minus(opts.slippage) + : new BigNumber(1).plus(opts.slippage); + result.output = result.output.times(outputMultiplier).integerValue(); return fromIntermediateQuoteFillResult(result, quoteInfo); } @@ -151,8 +157,8 @@ export function fillQuoteOrders( break; } const { source, fillData } = fill; - const fee = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData); - result.gas += new BigNumber(fee).toNumber(); + const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData); + result.gas += new BigNumber(gas).toNumber(); result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; // Actual rates are rarely linear, so fill subfills individually to @@ -179,11 +185,17 @@ export function fillQuoteOrders( remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); } } - result.protocolFee = result.protocolFee.plus(protocolFeePerFillOrder); + // NOTE: V4 Limit orders have Protocol fees + const protocolFee = hasProtocolFee(fo.order) ? protocolFeePerFillOrder : ZERO_AMOUNT; + result.protocolFee = result.protocolFee.plus(protocolFee); } return result; } +function hasProtocolFee(o: OptimizedMarketOrder): boolean { + return o.type === FillQuoteTransformerOrderType.Limit; +} + function solveForInputFillAmount( remainingInput: BigNumber, fillableInput: BigNumber, @@ -221,79 +233,33 @@ function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderC order: o, ...(side === MarketOperation.Sell ? { - totalOrderInput: o.takerAssetAmount, - totalOrderOutput: o.makerAssetAmount, - totalOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT, - totalOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT, + totalOrderInput: o.takerAmount, + totalOrderOutput: o.makerAmount, + totalOrderInputFee: + o.type === FillQuoteTransformerOrderType.Limit + ? getNativeAdjustedTakerFeeAmount( + (o.fillData as NativeLimitOrderFillData).order, + o.takerAmount, + ) + : ZERO_AMOUNT, + totalOrderOutputFee: ZERO_AMOUNT, // makerToken fees are not supported in v4 (sell output) } : // Buy { - totalOrderInput: o.makerAssetAmount, - totalOrderOutput: o.takerAssetAmount, - totalOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT, - totalOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT, + totalOrderInput: o.makerAmount, + totalOrderOutput: o.takerAmount, + totalOrderInputFee: ZERO_AMOUNT, // makerToken fees are not supported in v4 (buy input) + totalOrderOutputFee: + o.type === FillQuoteTransformerOrderType.Limit + ? getNativeAdjustedTakerFeeAmount( + (o.fillData as NativeLimitOrderFillData).order, + o.takerAmount, + ) + : ZERO_AMOUNT, }), })); } -function createWorstCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { - // Reuse best case fill orders, but apply slippage. - return ( - createBestCaseFillOrderCalls(quoteInfo) - .map(fo => ({ - ...fo, - order: { - ...fo.order, - // Apply slippage to order fills and reverse them. - fills: getSlippedOrderFills(fo.order, quoteInfo.side) - .map(f => ({ ...f, subFills: f.subFills.slice().reverse() })) - .reverse(), - }, - })) - // Sort by ascending price. - .sort((a, b) => - a.order.makerAssetAmount - .div(a.order.takerAssetAmount) - .comparedTo(b.order.makerAssetAmount.div(b.order.takerAssetAmount)), - ) - ); -} - -// Apply order slippage to its fill paths. -function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation): CollapsedFill[] { - // Infer the slippage from the order amounts vs fill amounts. - let inputScaling: BigNumber; - let outputScaling: BigNumber; - const source = order.fills[0].source; - if (source === ERC20BridgeSource.Native) { - // Native orders do not have slippage applied to them. - inputScaling = new BigNumber(1); - outputScaling = new BigNumber(1); - } else { - if (side === MarketOperation.Sell) { - const totalFillableTakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.input)); - const totalFillableMakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.output)); - inputScaling = order.fillableTakerAssetAmount.div(totalFillableTakerAssetAmount); - outputScaling = order.fillableMakerAssetAmount.div(totalFillableMakerAssetAmount); - } else { - const totalFillableTakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.output)); - const totalFillableMakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.input)); - inputScaling = order.fillableMakerAssetAmount.div(totalFillableMakerAssetAmount); - outputScaling = order.fillableTakerAssetAmount.div(totalFillableTakerAssetAmount); - } - } - return order.fills.map(f => ({ - ...f, - input: f.input.times(inputScaling), - output: f.output.times(outputScaling), - subFills: f.subFills.map(sf => ({ - ...sf, - input: sf.input.times(inputScaling), - output: sf.output.times(outputScaling), - })), - })); -} - function roundInputAmount(amount: BigNumber, side: MarketOperation): BigNumber { return amount.integerValue(side === MarketOperation.Sell ? ROUND_UP : ROUND_DOWN); } @@ -349,15 +315,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI }; } -function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { - const fills: CollapsedFill[] = []; - for (const o of orders) { - fills.push(...o.fills); - } - return fills; -} - -function getTotalGasUsedByFills(fills: CollapsedFill[], gasSchedule: FeeSchedule): number { +function getTotalGasUsedByFills(fills: OptimizedMarketOrder[], gasSchedule: FeeSchedule): number { let gasUsed = 0; for (const f of fills) { const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData); diff --git a/packages/asset-swapper/src/utils/rfqt_mocker.ts b/packages/asset-swapper/src/utils/rfqt_mocker.ts index 81c89d3d91..96f72c2bd9 100644 --- a/packages/asset-swapper/src/utils/rfqt_mocker.ts +++ b/packages/asset-swapper/src/utils/rfqt_mocker.ts @@ -1,7 +1,12 @@ import axios, { AxiosInstance } from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; -import { MockedRfqtFirmQuoteResponse } from '../types'; +import { MockedRfqtQuoteResponse } from '../types'; + +export enum RfqtQuoteEndpoint { + Indicative = 'price', + Firm = 'quote', +} /** * A helper utility for testing which mocks out @@ -9,15 +14,15 @@ import { MockedRfqtFirmQuoteResponse } from '../types'; */ export const rfqtMocker = { /** - * Stubs out responses from RFQ-T providers by mocking out - * HTTP calls via axios. Always restores the mock adapter - * after executing the `performFn`. + * A helper utility for testing which mocks out + * requests to RFQ-t providers */ - withMockedRfqtFirmQuotes: async ( - mockedResponses: MockedRfqtFirmQuoteResponse[], - performFn: () => Promise, + withMockedRfqtQuotes: async ( + mockedResponses: MockedRfqtQuoteResponse[], + quoteType: RfqtQuoteEndpoint, + afterResponseCallback: () => Promise, axiosClient: AxiosInstance = axios, - ) => { + ): Promise => { const mockedAxios = new AxiosMockAdapter(axiosClient); try { // Mock out RFQT responses @@ -25,33 +30,11 @@ export const rfqtMocker = { const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; mockedAxios - .onGet(`${endpoint}/quote`, { params: requestParams }, requestHeaders) + .onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders) .replyOnce(responseCode, responseData); } - - await performFn(); - } finally { - // Ensure we always restore axios afterwards - mockedAxios.restore(); - } - }, - withMockedRfqtIndicativeQuotes: async ( - mockedResponses: MockedRfqtFirmQuoteResponse[], - performFn: () => Promise, - axiosClient: AxiosInstance = axios, - ) => { - const mockedAxios = new AxiosMockAdapter(axiosClient); - try { - // Mock out RFQT responses - for (const mockedResponse of mockedResponses) { - const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; - const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; - mockedAxios - .onGet(`${endpoint}/price`, { params: requestParams }, requestHeaders) - .replyOnce(responseCode, responseData); - } - - await performFn(); + // Perform the callback function, e.g. a test validation + await afterResponseCallback(); } finally { // Ensure we always restore axios afterwards mockedAxios.restore(); diff --git a/packages/asset-swapper/src/utils/sorting_utils.ts b/packages/asset-swapper/src/utils/sorting_utils.ts deleted file mode 100644 index 84554392e8..0000000000 --- a/packages/asset-swapper/src/utils/sorting_utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { schemas } from '@0x/json-schemas'; -import { Order } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; - -import { assert } from './assert'; -import { getAdjustedMakerAndTakerAmountsFromTakerFees } from './utils'; - -export const sortingUtils = { - sortOrders(orders: T[]): T[] { - assert.doesConformToSchema('orders', orders, schemas.ordersSchema); - assert.isValidOrdersForSwapQuoter('orders', orders); - const copiedOrders = _.cloneDeep(orders); - copiedOrders.sort((firstOrder, secondOrder) => { - const firstOrderRate = getTakerFeeAdjustedRateOfOrder(firstOrder); - const secondOrderRate = getTakerFeeAdjustedRateOfOrder(secondOrder); - return firstOrderRate.comparedTo(secondOrderRate); - }); - return copiedOrders; - }, -}; - -function getTakerFeeAdjustedRateOfOrder(order: Order): BigNumber { - const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = getAdjustedMakerAndTakerAmountsFromTakerFees(order); - const rate = adjustedTakerAssetAmount.div(adjustedMakerAssetAmount); - return rate; -} diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts deleted file mode 100644 index fdf79ba50b..0000000000 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; - -import { constants } from '../constants'; -import { - CalculateSwapQuoteOpts, - MarketBuySwapQuote, - MarketOperation, - MarketSellSwapQuote, - SwapQuote, - SwapQuoteInfo, - SwapQuoteOrdersBreakdown, - SwapQuoterError, -} from '../types'; - -import { MarketOperationUtils } from './market_operation_utils'; -import { SOURCE_FLAGS } from './market_operation_utils/constants'; -import { - ERC20BridgeSource, - FeeSchedule, - FillData, - GetMarketOrdersOpts, - OptimizedMarketOrder, - OptimizerResultWithReport, -} from './market_operation_utils/types'; -import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; -import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils'; - -// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? -export class SwapQuoteCalculator { - private readonly _marketOperationUtils: MarketOperationUtils; - - constructor(marketOperationUtils: MarketOperationUtils) { - this._marketOperationUtils = marketOperationUtils; - } - - public async calculateMarketSellSwapQuoteAsync( - prunedOrders: SignedOrder[], - takerAssetFillAmount: BigNumber, - gasPrice: BigNumber, - opts: CalculateSwapQuoteOpts, - ): Promise { - return (await this._calculateSwapQuoteAsync( - prunedOrders, - takerAssetFillAmount, - gasPrice, - MarketOperation.Sell, - opts, - )) as MarketSellSwapQuote; - } - - public async calculateMarketBuySwapQuoteAsync( - prunedOrders: SignedOrder[], - takerAssetFillAmount: BigNumber, - gasPrice: BigNumber, - opts: CalculateSwapQuoteOpts, - ): Promise { - return (await this._calculateSwapQuoteAsync( - prunedOrders, - takerAssetFillAmount, - gasPrice, - MarketOperation.Buy, - opts, - )) as MarketBuySwapQuote; - } - - public async calculateBatchMarketBuySwapQuoteAsync( - batchPrunedOrders: SignedOrder[][], - takerAssetFillAmounts: BigNumber[], - gasPrice: BigNumber, - opts: CalculateSwapQuoteOpts, - ): Promise> { - return (await this._calculateBatchBuySwapQuoteAsync( - batchPrunedOrders, - takerAssetFillAmounts, - gasPrice, - MarketOperation.Buy, - opts, - )) as Array; - } - - private async _calculateBatchBuySwapQuoteAsync( - batchPrunedOrders: SignedOrder[][], - assetFillAmounts: BigNumber[], - gasPrice: BigNumber, - operation: MarketOperation, - opts: CalculateSwapQuoteOpts, - ): Promise> { - const optimizerResults = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync( - batchPrunedOrders, - assetFillAmounts, - opts, - ); - - const batchSwapQuotes = await Promise.all( - optimizerResults.map(async (result, i) => { - if (result) { - const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0]; - return createSwapQuote( - result, - makerAssetData, - takerAssetData, - operation, - assetFillAmounts[i], - gasPrice, - opts.gasSchedule, - ); - } else { - return undefined; - } - }), - ); - return batchSwapQuotes; - } - private async _calculateSwapQuoteAsync( - prunedOrders: SignedOrder[], - assetFillAmount: BigNumber, - gasPrice: BigNumber, - operation: MarketOperation, - opts: CalculateSwapQuoteOpts, - ): Promise { - // checks if maker asset is ERC20 and taker asset is ERC20 - if (!isSupportedAssetDataInOrders(prunedOrders)) { - throw Error(SwapQuoterError.AssetDataUnsupported); - } - // since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled - - // Scale fees by gas price. - const _opts: GetMarketOrdersOpts = { - ...opts, - feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => - gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), - ), - exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), - }; - - const result = - operation === MarketOperation.Buy - ? await this._marketOperationUtils.getMarketBuyOrdersAsync(prunedOrders, assetFillAmount, _opts) - : await this._marketOperationUtils.getMarketSellOrdersAsync(prunedOrders, assetFillAmount, _opts); - - const { makerAssetData, takerAssetData } = prunedOrders[0]; - const swapQuote = createSwapQuote( - result, - makerAssetData, - takerAssetData, - operation, - assetFillAmount, - gasPrice, - opts.gasSchedule, - ); - - // Use the raw gas, not scaled by gas price - const exchangeProxyOverhead = opts.exchangeProxyOverhead(result.sourceFlags).toNumber(); - swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead; - swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead; - swapQuote.unoptimizedQuoteInfo.gas += exchangeProxyOverhead; - - return swapQuote; - } -} - -function createSwapQuote( - optimizerResult: OptimizerResultWithReport, - makerAssetData: string, - takerAssetData: string, - operation: MarketOperation, - assetFillAmount: BigNumber, - gasPrice: BigNumber, - gasSchedule: FeeSchedule, -): SwapQuote { - const { - optimizedOrders, - quoteReport, - sourceFlags, - unoptimizedPath, - takerAssetToEthRate, - makerAssetToEthRate, - } = optimizerResult; - const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]; - - // Calculate quote info - const { bestCaseQuoteInfo, worstCaseQuoteInfo, sourceBreakdown } = isTwoHop - ? calculateTwoHopQuoteInfo(optimizedOrders, operation, gasSchedule) - : calculateQuoteInfo(optimizedOrders, operation, assetFillAmount, gasPrice, gasSchedule); - - // Calculate the unoptimised alternative - const unoptimizedOrders = unoptimizedPath !== undefined ? unoptimizedPath.orders : []; - const unoptimizedFillResult = simulateBestCaseFill({ - gasPrice, - orders: unoptimizedOrders, - side: operation, - fillAmount: assetFillAmount, - opts: { gasSchedule }, - }); - const unoptimizedQuoteInfo = fillResultsToQuoteInfo(unoptimizedFillResult); - - // Put together the swap quote - const { makerTokenDecimals, takerTokenDecimals } = optimizerResult.marketSideLiquidity; - const swapQuote = { - makerAssetData, - takerAssetData, - gasPrice, - orders: optimizedOrders, - bestCaseQuoteInfo, - worstCaseQuoteInfo, - unoptimizedQuoteInfo, - unoptimizedOrders, - sourceBreakdown, - makerTokenDecimals, - takerTokenDecimals, - takerAssetToEthRate, - makerAssetToEthRate, - quoteReport, - isTwoHop, - }; - - if (operation === MarketOperation.Buy) { - return { - ...swapQuote, - type: MarketOperation.Buy, - makerAssetFillAmount: assetFillAmount, - }; - } else { - return { - ...swapQuote, - type: MarketOperation.Sell, - takerAssetFillAmount: assetFillAmount, - }; - } -} - -function calculateQuoteInfo( - optimizedOrders: OptimizedMarketOrder[], - operation: MarketOperation, - assetFillAmount: BigNumber, - gasPrice: BigNumber, - gasSchedule: FeeSchedule, -): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { - const bestCaseFillResult = simulateBestCaseFill({ - gasPrice, - orders: optimizedOrders, - side: operation, - fillAmount: assetFillAmount, - opts: { gasSchedule }, - }); - - const worstCaseFillResult = simulateWorstCaseFill({ - gasPrice, - orders: optimizedOrders, - side: operation, - fillAmount: assetFillAmount, - opts: { gasSchedule }, - }); - - return { - bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), - worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), - sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), - }; -} - -function calculateTwoHopQuoteInfo( - optimizedOrders: OptimizedMarketOrder[], - operation: MarketOperation, - gasSchedule: FeeSchedule, -): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { - const [firstHopOrder, secondHopOrder] = optimizedOrders; - const [firstHopFill] = firstHopOrder.fills; - const [secondHopFill] = secondHopOrder.fills; - const gas = new BigNumber( - gasSchedule[ERC20BridgeSource.MultiHop]!({ - firstHopSource: _.pick(firstHopFill, 'source', 'fillData'), - secondHopSource: _.pick(secondHopFill, 'source', 'fillData'), - }), - ).toNumber(); - - return { - bestCaseQuoteInfo: { - makerAssetAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input, - takerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output, - totalTakerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output, - feeTakerAssetAmount: constants.ZERO_AMOUNT, - protocolFeeInWeiAmount: constants.ZERO_AMOUNT, - gas, - }, - worstCaseQuoteInfo: { - makerAssetAmount: secondHopOrder.makerAssetAmount, - takerAssetAmount: firstHopOrder.takerAssetAmount, - totalTakerAssetAmount: firstHopOrder.takerAssetAmount, - feeTakerAssetAmount: constants.ZERO_AMOUNT, - protocolFeeInWeiAmount: constants.ZERO_AMOUNT, - gas, - }, - sourceBreakdown: { - [ERC20BridgeSource.MultiHop]: { - proportion: new BigNumber(1), - intermediateToken: getTokenFromAssetData(secondHopOrder.takerAssetData), - hops: [firstHopFill.source, secondHopFill.source], - }, - }, - }; -} - -function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown { - const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); - const breakdown: SwapQuoteOrdersBreakdown = {}; - Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => { - breakdown[source as keyof SwapQuoteOrdersBreakdown] = fillAmount.div(totalFillAmount); - }); - return breakdown; -} - -function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo { - return { - makerAssetAmount: fr.totalMakerAssetAmount, - takerAssetAmount: fr.takerAssetAmount, - totalTakerAssetAmount: fr.totalTakerAssetAmount, - feeTakerAssetAmount: fr.takerFeeTakerAssetAmount, - protocolFeeInWeiAmount: fr.protocolFeeAmount, - gas: fr.gas, - }; -} diff --git a/packages/asset-swapper/src/utils/swap_quote_consumer_utils.ts b/packages/asset-swapper/src/utils/swap_quote_consumer_utils.ts deleted file mode 100644 index 075c770750..0000000000 --- a/packages/asset-swapper/src/utils/swap_quote_consumer_utils.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { WETH9Contract } from '@0x/contract-wrappers'; -import { assetDataUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper'; -import { Provider } from 'ethereum-types'; -import * as _ from 'lodash'; - -import { constants } from '../constants'; -import { - ExtensionContractType, - GetExtensionContractTypeOpts, - SwapQuote, - SwapQuoteConsumerError, - SwapQuoteExecutionOpts, -} from '../types'; - -import { assert } from './assert'; -import { isExactAssetData } from './utils'; - -export const swapQuoteConsumerUtils = { - async getTakerAddressOrThrowAsync( - provider: SupportedProvider, - opts: Partial, - ): Promise { - const takerAddress = await swapQuoteConsumerUtils.getTakerAddressAsync(provider, opts); - if (takerAddress === undefined) { - throw new Error(SwapQuoteConsumerError.NoAddressAvailable); - } else { - return takerAddress; - } - }, - async getTakerAddressAsync( - provider: SupportedProvider, - opts: Partial, - ): Promise { - if (opts.takerAddress !== undefined) { - return opts.takerAddress; - } else { - const web3Wrapper = new Web3Wrapper(provider); - const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); - const firstAvailableAddress = _.head(availableAddresses); - if (firstAvailableAddress !== undefined) { - return firstAvailableAddress; - } else { - return undefined; - } - } - }, - async getEthAndWethBalanceAsync( - provider: SupportedProvider, - contractAddresses: ContractAddresses, - takerAddress: string, - ): Promise<[BigNumber, BigNumber]> { - const weth = new WETH9Contract(contractAddresses.etherToken, provider); - const web3Wrapper = new Web3Wrapper(provider); - const ethBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const wethBalance = await weth.balanceOf(takerAddress).callAsync(); - return [ethBalance, wethBalance]; - }, - isValidForwarderSwapQuote(swapQuote: SwapQuote, wethAssetData: string): boolean { - return swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.orders, wethAssetData); - }, - isValidForwarderSignedOrders(orders: SignedOrder[], wethAssetData: string): boolean { - return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData)); - }, - isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean { - return isExactAssetData(order.takerAssetData, wethAssetData); - }, - async getExtensionContractTypeForSwapQuoteAsync( - quote: SwapQuote, - contractAddresses: ContractAddresses, - provider: Provider, - opts: Partial, - ): Promise { - const wethAssetData = assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken); - if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) { - if (opts.takerAddress !== undefined) { - assert.isETHAddressHex('takerAddress', opts.takerAddress); - } - const ethAmount = - opts.ethAmount || - quote.worstCaseQuoteInfo.takerAssetAmount.plus(quote.worstCaseQuoteInfo.protocolFeeInWeiAmount); - const takerAddress = await swapQuoteConsumerUtils.getTakerAddressAsync(provider, opts); - const takerEthAndWethBalance = - takerAddress !== undefined - ? await swapQuoteConsumerUtils.getEthAndWethBalanceAsync(provider, contractAddresses, takerAddress) - : [constants.ZERO_AMOUNT, constants.ZERO_AMOUNT]; - // TODO(david): when considering if there is enough Eth balance, should account for gas costs. - const isEnoughEthAndWethBalance = _.map(takerEthAndWethBalance, (balance: BigNumber) => - balance.isGreaterThanOrEqualTo(ethAmount), - ); - if (isEnoughEthAndWethBalance[1]) { - // should be more gas efficient to use exchange consumer, so if possible use it. - return ExtensionContractType.None; - } else if (isEnoughEthAndWethBalance[0] && !isEnoughEthAndWethBalance[1]) { - return ExtensionContractType.Forwarder; - } - // Note: defaulting to forwarderConsumer if takerAddress is null or not enough balance of either wEth or Eth - return ExtensionContractType.Forwarder; - } else { - return ExtensionContractType.None; - } - }, -}; diff --git a/packages/asset-swapper/src/utils/utils.ts b/packages/asset-swapper/src/utils/utils.ts index 0b1ecc277c..be7a7570ff 100644 --- a/packages/asset-swapper/src/utils/utils.ts +++ b/packages/asset-swapper/src/utils/utils.ts @@ -1,134 +1,100 @@ -import { assetDataUtils } from '@0x/order-utils'; -import { AssetData, AssetProxyId, ERC20AssetData, ERC20BridgeAssetData, Order, SignedOrder } from '@0x/types'; -import { BigNumber, NULL_BYTES } from '@0x/utils'; +import { CommonOrderFields, FillQuoteTransformerOrderType, LimitOrderFields } from '@0x/protocol-utils'; +import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { constants } from '../constants'; -import { PriceAwareRFQFlags } from '../types'; +import { NativeOrderFillableAmountFields, SignedNativeOrder } from '../types'; + +import { ZERO_AMOUNT } from './market_operation_utils/constants'; // tslint:disable: no-unnecessary-type-assertion completed-docs -/** - * Returns 2 flags (one for firm quotes and another for indicative quotes) that serve as rollout flags for the price-aware RFQ feature. - * By default, indicative quotes should *always* go through the new price-aware flow. This means that all indicative RFQ requests made to - * market makers will contain the new price-aware `suggestedPrice` field. - * The `isPriceAwareRFQEnabled` feature object that is passed in by the 0x API will then control whether firm quotes go through price-aware RFQ. - * - * @param isPriceAwareRFQEnabled the feature flag that is passed in by the 0x API. - */ -export function getPriceAwareRFQRolloutFlags(priceAwareRFQFlags?: PriceAwareRFQFlags): PriceAwareRFQFlags { - return priceAwareRFQFlags !== undefined - ? priceAwareRFQFlags - : { - isFirmPriceAwareEnabled: false, - isIndicativePriceAwareEnabled: false, - }; -} - -export function isSupportedAssetDataInOrders(orders: SignedOrder[]): boolean { - const firstOrderMakerAssetData = !!orders[0] - ? assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData) - : { assetProxyId: '' }; - return orders.every(o => { - const takerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.takerAssetData); - const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.makerAssetData); - return ( - makerAssetData.assetProxyId === AssetProxyId.ERC20 && - takerAssetData.assetProxyId === AssetProxyId.ERC20 && - firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId - ); // checks that all native order maker assets are of the same type - }); -} - export function numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber { return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(percentage); } -export function isOrderTakerFeePayableWithMakerAsset(order: T): boolean { - return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.makerAssetData); -} - -export function isOrderTakerFeePayableWithTakerAsset(order: T): boolean { - return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.takerAssetData); -} - -export function getAdjustedMakerAndTakerAmountsFromTakerFees(order: T): [BigNumber, BigNumber] { - const adjustedMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order) - ? order.makerAssetAmount.minus(order.takerFee) - : order.makerAssetAmount; - const adjustedTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order) - ? order.takerAssetAmount.plus(order.takerFee) - : order.takerAssetAmount; - return [adjustedMakerAssetAmount, adjustedTakerAssetAmount]; -} - -export function isExactAssetData(expectedAssetData: string, actualAssetData: string): boolean { - return expectedAssetData === actualAssetData; +export function getAdjustedTakerAmountFromFees(order: T): BigNumber { + return order.takerAmount.plus(order.takerTokenFeeAmount); } /** - * Compare the Asset Data for equivalency. Expected is the asset data the user provided (wanted), - * actual is the asset data found or created. + * Given an amount of taker asset, calculate the the amount of maker asset + * @param order The order + * @param makerFillAmount the amount of taker asset */ -export function isAssetDataEquivalent(expectedAssetData: string, actualAssetData: string): boolean { - if (isExactAssetData(expectedAssetData, actualAssetData)) { - return true; - } - const decodedExpectedAssetData = assetDataUtils.decodeAssetDataOrThrow(expectedAssetData); - const decodedActualAssetData = assetDataUtils.decodeAssetDataOrThrow(actualAssetData); - // ERC20 === ERC20, ERC20 === ERC20Bridge - if (isERC20EquivalentAssetData(decodedExpectedAssetData) && isERC20EquivalentAssetData(decodedActualAssetData)) { - const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress; - return doesTokenAddressMatch; - } - // ERC1155 === ERC1155 - if ( - assetDataUtils.isERC1155TokenAssetData(decodedExpectedAssetData) && - assetDataUtils.isERC1155TokenAssetData(decodedActualAssetData) - ) { - const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress; - // IDs may be out of order yet still equivalent - // i.e (["a", "b"], [1,2]) === (["b", "a"], [2, 1]) - // (["a", "b"], [2,1]) !== (["b", "a"], [2, 1]) - const hasAllIds = decodedExpectedAssetData.tokenIds.every( - id => decodedActualAssetData.tokenIds.findIndex(v => id.eq(v)) !== -1, - ); - const hasAllValues = decodedExpectedAssetData.tokenIds.every((id, i) => - decodedExpectedAssetData.tokenValues[i].eq( - decodedActualAssetData.tokenValues[decodedActualAssetData.tokenIds.findIndex(v => id.eq(v))], - ), - ); - // If expected contains callback data, ensure it is present - // if actual has callbackdata and expected provided none then ignore it - const hasEquivalentCallback = - decodedExpectedAssetData.callbackData === NULL_BYTES || - decodedExpectedAssetData.callbackData === decodedActualAssetData.callbackData; - return doesTokenAddressMatch && hasAllIds && hasAllValues && hasEquivalentCallback; - } - // ERC721 === ERC721 - if ( - assetDataUtils.isERC721TokenAssetData(decodedExpectedAssetData) || - assetDataUtils.isERC721TokenAssetData(decodedActualAssetData) - ) { - // Asset Data should exactly match for ERC721 - return isExactAssetData(expectedAssetData, actualAssetData); - } - - // TODO(dekz): Unsupported cases - // ERCXX(token) === MAP(token, staticCall) - // MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall) - return false; +export function getNativeAdjustedMakerFillAmount(order: CommonOrderFields, takerFillAmount: BigNumber): BigNumber { + // Round down because exchange rate favors Maker + const makerFillAmount = takerFillAmount + .multipliedBy(order.makerAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_FLOOR); + return makerFillAmount; +} +/** + * Given an amount of maker asset, calculate the equivalent amount in taker asset + * @param order The order + * @param makerFillAmount the amount of maker asset + */ +export function getNativeAdjustedTakerFillAmount(order: CommonOrderFields, makerFillAmount: BigNumber): BigNumber { + // Round up because exchange rate favors Maker + const takerFillAmount = makerFillAmount + .multipliedBy(order.takerAmount) + .div(order.makerAmount) + .integerValue(BigNumber.ROUND_CEIL); + return takerFillAmount; } -export function isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData { - return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData); +/** + * Given an amount of taker asset, calculate the fee amount required for the taker + * @param order The order + * @param takerFillAmount the amount of taker asset + */ +export function getNativeAdjustedTakerFeeAmount(order: LimitOrderFields, takerFillAmount: BigNumber): BigNumber { + // Round down because Taker fee rate favors Taker + const takerFeeAmount = takerFillAmount + .multipliedBy(order.takerTokenFeeAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_FLOOR); + return takerFeeAmount; } -export function getTokenFromAssetData(assetData: string): string { - const data = assetDataUtils.decodeAssetDataOrThrow(assetData); - if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) { - throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`); +const EMPTY_FILLABLE_AMOUNTS: NativeOrderFillableAmountFields = { + fillableMakerAmount: ZERO_AMOUNT, + fillableTakerAmount: ZERO_AMOUNT, + fillableTakerFeeAmount: ZERO_AMOUNT, +}; + +export function getNativeAdjustedFillableAmountsFromTakerAmount( + order: SignedNativeOrder, + takerFillableAmount: BigNumber, +): NativeOrderFillableAmountFields { + if (takerFillableAmount.isZero()) { + return EMPTY_FILLABLE_AMOUNTS; } - // tslint:disable-next-line:no-unnecessary-type-assertion - return (data as ERC20AssetData).tokenAddress; + return { + fillableTakerAmount: takerFillableAmount, + fillableMakerAmount: getNativeAdjustedMakerFillAmount(order.order, takerFillableAmount), + fillableTakerFeeAmount: + order.type === FillQuoteTransformerOrderType.Limit + ? getNativeAdjustedTakerFeeAmount(order.order as LimitOrderFields, takerFillableAmount) + : ZERO_AMOUNT, + }; +} + +export function getNativeAdjustedFillableAmountsFromMakerAmount( + order: SignedNativeOrder, + makerFillableAmount: BigNumber, +): NativeOrderFillableAmountFields { + if (makerFillableAmount.isZero()) { + return EMPTY_FILLABLE_AMOUNTS; + } + const takerFillableAmount = getNativeAdjustedTakerFillAmount(order.order, makerFillableAmount); + return { + fillableMakerAmount: makerFillableAmount, + fillableTakerAmount: takerFillableAmount, + fillableTakerFeeAmount: + order.type === FillQuoteTransformerOrderType.Limit + ? getNativeAdjustedTakerFeeAmount(order.order as LimitOrderFields, takerFillableAmount) + : ZERO_AMOUNT, + }; } diff --git a/packages/asset-swapper/test/artifacts.ts b/packages/asset-swapper/test/artifacts.ts index 9a3b642278..331a918b4d 100644 --- a/packages/asset-swapper/test/artifacts.ts +++ b/packages/asset-swapper/test/artifacts.ts @@ -40,6 +40,7 @@ import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeO import * as TwoHopSampler from '../test/generated-artifacts/TwoHopSampler.json'; import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json'; import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json'; +import * as UtilitySampler from '../test/generated-artifacts/UtilitySampler.json'; export const artifacts = { ApproximateBuys: ApproximateBuys as ContractArtifact, BalanceChecker: BalanceChecker as ContractArtifact, @@ -62,6 +63,7 @@ export const artifacts = { TwoHopSampler: TwoHopSampler as ContractArtifact, UniswapSampler: UniswapSampler as ContractArtifact, UniswapV2Sampler: UniswapV2Sampler as ContractArtifact, + UtilitySampler: UtilitySampler as ContractArtifact, IBalancer: IBalancer as ContractArtifact, IBancor: IBancor as ContractArtifact, ICurve: ICurve as ContractArtifact, diff --git a/packages/asset-swapper/test/calculate_liquidity_test.ts b/packages/asset-swapper/test/calculate_liquidity_test.ts deleted file mode 100644 index 3f20737c48..0000000000 --- a/packages/asset-swapper/test/calculate_liquidity_test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as chai from 'chai'; -import * as _ from 'lodash'; -import 'mocha'; - -import { calculateLiquidity } from '../src/utils/calculate_liquidity'; - -import { chaiSetup } from './utils/chai_setup'; -import { testOrders } from './utils/test_orders'; -import { baseUnitAmount } from './utils/utils'; - -chaiSetup.configure(); -const expect = chai.expect; -const { - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, -} = testOrders; - -// tslint:disable:custom-no-magic-numbers -describe('#calculateLiquidity', () => { - it('should provide correct liquidity result with feeless orders', () => { - const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS; - const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( - prunedSignedOrders, - ); - expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11)); - expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9)); - }); - it('should provide correct liquidity result with orders with takerFees in takerAsset', () => { - const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET; - const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( - prunedSignedOrders, - ); - expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11)); - expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15)); - }); - it('should provide correct liquidity result with orders with takerFees in makerAsset', () => { - const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET; - const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( - prunedSignedOrders, - ); - expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(5)); - expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9)); - }); - it('should provide correct liquidity result with mixed orders with fees and no fees', () => { - const prunedSignedOrders = _.concat( - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( - prunedSignedOrders, - ); - expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(27)); - expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(33)); - }); -}); diff --git a/packages/asset-swapper/test/comparison_price_test.ts b/packages/asset-swapper/test/comparison_price_test.ts index 8214c39ed7..e34ab5e643 100644 --- a/packages/asset-swapper/test/comparison_price_test.ts +++ b/packages/asset-swapper/test/comparison_price_test.ts @@ -49,12 +49,14 @@ const buyMarketSideLiquidity: MarketSideLiquidity = { inputAmount: new BigNumber(0), inputToken: ETH_TOKEN, outputToken: DAI_TOKEN, - dexQuotes: [dexQuotes], - nativeOrders: [], - orderFillableAmounts: [], - twoHopQuotes: [], - rfqtIndicativeQuotes: [], + quotes: { + twoHopQuotes: [], + rfqtIndicativeQuotes: [], + dexQuotes: [dexQuotes], + nativeOrders: [], + }, quoteSourceFilters: new SourceFilters(), + isRfqSupported: false, }; const sellMarketSideLiquidity: MarketSideLiquidity = { @@ -68,12 +70,14 @@ const sellMarketSideLiquidity: MarketSideLiquidity = { inputAmount: new BigNumber(0), inputToken: ETH_TOKEN, outputToken: DAI_TOKEN, - dexQuotes: [dexQuotes], - nativeOrders: [], - orderFillableAmounts: [], - twoHopQuotes: [], - rfqtIndicativeQuotes: [], + quotes: { + dexQuotes: [dexQuotes], + nativeOrders: [], + twoHopQuotes: [], + rfqtIndicativeQuotes: [], + }, quoteSourceFilters: new SourceFilters(), + isRfqSupported: false, }; describe('getComparisonPrices', async () => { diff --git a/packages/asset-swapper/test/contracts/balance_checker_test.ts b/packages/asset-swapper/test/contracts/balance_checker_test.ts index 0ea3bb5e94..a1088344a0 100644 --- a/packages/asset-swapper/test/contracts/balance_checker_test.ts +++ b/packages/asset-swapper/test/contracts/balance_checker_test.ts @@ -57,4 +57,76 @@ blockchainTests.resets('BalanceChecker contract', env => { } }); }); + describe('getMinOfBalancesOrAllowances', () => { + it('returns the balance if the allowance can cover it', async () => { + const makerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + erc20Artifacts.DummyERC20Token, + env.provider, + env.txDefaults, + artifacts, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + new BigNumber(18), + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const owner = accounts[0]; + const owner2 = accounts[1]; + + const allowanceTarget = '0xdef1c0ded9bec7f1a1670819833240f027b25eff'; + + await makerToken.mint(new BigNumber(100)).awaitTransactionSuccessAsync({ from: owner }); + await makerToken.approve(allowanceTarget, new BigNumber(150)).awaitTransactionSuccessAsync({ from: owner }); + + await makerToken.mint(new BigNumber(150)).awaitTransactionSuccessAsync({ from: owner2 }); + await makerToken + .approve(allowanceTarget, new BigNumber(200)) + .awaitTransactionSuccessAsync({ from: owner2 }); + + const testResults = await contract + .getMinOfBalancesOrAllowances( + [owner, owner2], + [makerToken.address, makerToken.address], + allowanceTarget, + ) + .callAsync(); + + expect(testResults).to.eql([new BigNumber(100), new BigNumber(150)]); + }); + it('returns the allowance if the allowance < balance', async () => { + const makerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + erc20Artifacts.DummyERC20Token, + env.provider, + env.txDefaults, + artifacts, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + new BigNumber(18), + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const owner = accounts[0]; + const owner2 = accounts[1]; + + const allowanceTarget = '0xdef1c0ded9bec7f1a1670819833240f027b25eff'; + + await makerToken.mint(new BigNumber(100)).awaitTransactionSuccessAsync({ from: owner }); + await makerToken.approve(allowanceTarget, new BigNumber(50)).awaitTransactionSuccessAsync({ from: owner }); + + await makerToken.mint(new BigNumber(100)).awaitTransactionSuccessAsync({ from: owner2 }); + await makerToken.approve(allowanceTarget, new BigNumber(75)).awaitTransactionSuccessAsync({ from: owner2 }); + + const testResults = await contract + .getMinOfBalancesOrAllowances( + [owner, owner2], + [makerToken.address, makerToken.address], + allowanceTarget, + ) + .callAsync(); + + expect(testResults).to.eql([new BigNumber(50), new BigNumber(75)]); + }); + }); }); diff --git a/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts b/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts index 8dc809cddf..fde261f51e 100644 --- a/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts +++ b/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts @@ -6,11 +6,12 @@ import { getRandomPortion, randomAddress, } from '@0x/contracts-test-utils'; -import { Order } from '@0x/types'; -import { BigNumber, hexUtils } from '@0x/utils'; +import { SignatureType } from '@0x/protocol-utils'; +import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils'; import * as _ from 'lodash'; -import { SamplerCallResult } from '../../src/types'; +import { FillQuoteTransformerOrderType, LimitOrderFields } from '../../src'; +import { SamplerCallResult, SignedNativeOrder } from '../../src/types'; import { artifacts } from '../artifacts'; import { DummyLiquidityProviderContract, TestERC20BridgeSamplerContract } from '../wrappers'; @@ -30,7 +31,7 @@ blockchainTests('erc20-bridge-sampler', env => { const ETH2DAI_SALT = '0xb713b61bb9bb2958a0f5d1534b21e94fc68c4c0c034b0902ed844f2f6cd1b4f7'; const UNISWAP_BASE_SALT = '0x1d6a6a0506b0b4a554b907a4c29d9f4674e461989d9c1921feb17b26716385ab'; const UNISWAP_V2_SALT = '0xadc7fcb33c735913b8635927e66896b356a53a912ab2ceff929e60a04b53b3c1'; - const ERC20_PROXY_ID = '0xf47261b0'; + let UNISWAP_V2_ROUTER = ''; const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR'; const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); @@ -44,6 +45,7 @@ blockchainTests('erc20-bridge-sampler', env => { env.txDefaults, {}, ); + UNISWAP_V2_ROUTER = await testContract.uniswapV2Router().callAsync(); }); function getPackedHash(...args: string[]): string { @@ -207,23 +209,19 @@ blockchainTests('erc20-bridge-sampler', env => { return sold; } - function getDeterministicFillableTakerAssetAmount(order: Order): BigNumber { - const hash = getPackedHash(hexUtils.leftPad(order.salt)); - return new BigNumber(hash).mod(order.takerAssetAmount); + function getDeterministicFillableTakerAssetAmount(order: SignedNativeOrder): BigNumber { + const hash = getPackedHash(hexUtils.leftPad(order.order.salt)); + return new BigNumber(hash).mod(order.order.takerAmount); } - function getDeterministicFillableMakerAssetAmount(order: Order): BigNumber { + function getDeterministicFillableMakerAssetAmount(order: SignedNativeOrder): BigNumber { const takerAmount = getDeterministicFillableTakerAssetAmount(order); - return order.makerAssetAmount + return order.order.makerAmount .times(takerAmount) - .div(order.takerAssetAmount) + .div(order.order.takerAmount) .integerValue(BigNumber.ROUND_UP); } - function getERC20AssetData(tokenAddress: string): string { - return hexUtils.concat(ERC20_PROXY_ID, hexUtils.leftPad(tokenAddress)); - } - function getSampleAmounts(tokenAddress: string, count?: number): BigNumber[] { const tokenDecimals = getDeterministicTokenDecimals(tokenAddress); const _upperLimit = getRandomPortion(getRandomInteger(1000, 50000).times(10 ** tokenDecimals)); @@ -232,28 +230,30 @@ blockchainTests('erc20-bridge-sampler', env => { return _.times(_count, i => d.times((i + 1) / _count).integerValue()); } - function createOrder(makerToken: string, takerToken: string): Order { + function createOrder(makerToken: string, takerToken: string): SignedNativeOrder { return { - chainId: 1337, - exchangeAddress: randomAddress(), - makerAddress: randomAddress(), - takerAddress: randomAddress(), - senderAddress: randomAddress(), - feeRecipientAddress: randomAddress(), - makerAssetAmount: getRandomInteger(1, 1e18), - takerAssetAmount: getRandomInteger(1, 1e18), - makerFee: getRandomInteger(1, 1e18), - takerFee: getRandomInteger(1, 1e18), - makerAssetData: getERC20AssetData(makerToken), - takerAssetData: getERC20AssetData(takerToken), - makerFeeAssetData: getERC20AssetData(randomAddress()), - takerFeeAssetData: getERC20AssetData(randomAddress()), - salt: new BigNumber(hexUtils.random()), - expirationTimeSeconds: getRandomInteger(0, 2 ** 32), + order: { + chainId: 1337, + verifyingContract: randomAddress(), + maker: randomAddress(), + taker: randomAddress(), + pool: NULL_BYTES, + sender: NULL_ADDRESS, + feeRecipient: randomAddress(), + makerAmount: getRandomInteger(1, 1e18), + takerAmount: getRandomInteger(1, 1e18), + takerTokenFeeAmount: getRandomInteger(1, 1e18), + makerToken, + takerToken, + salt: new BigNumber(hexUtils.random()), + expiry: getRandomInteger(0, 2 ** 32), + }, + signature: { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign }, + type: FillQuoteTransformerOrderType.Limit, }; } - function createOrders(makerToken: string, takerToken: string, count?: number): Order[] { + function createOrders(makerToken: string, takerToken: string, count?: number): SignedNativeOrder[] { return _.times(count || _.random(1, 16), () => createOrder(makerToken, takerToken)); } @@ -291,16 +291,20 @@ blockchainTests('erc20-bridge-sampler', env => { describe('getOrderFillableTakerAssetAmounts()', () => { it('returns the expected amount for each order', async () => { const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); - const signatures: string[] = _.times(orders.length, i => hexUtils.random()); const expected = orders.map(getDeterministicFillableTakerAssetAmount); const actual = await testContract - .getOrderFillableTakerAssetAmounts(orders, signatures, NULL_ADDRESS) + .getLimitOrderFillableTakerAssetAmounts( + // tslint:disable-next-line:no-unnecessary-type-assertion + orders.map(o => o.order as LimitOrderFields), + orders.map(o => o.signature), + NULL_ADDRESS, + ) .callAsync(); expect(actual).to.deep.eq(expected); }); it('returns empty for no orders', async () => { - const actual = await testContract.getOrderFillableTakerAssetAmounts([], [], NULL_ADDRESS).callAsync(); + const actual = await testContract.getLimitOrderFillableTakerAssetAmounts([], [], NULL_ADDRESS).callAsync(); expect(actual).to.deep.eq([]); }); }); @@ -308,16 +312,20 @@ blockchainTests('erc20-bridge-sampler', env => { describe('getOrderFillableMakerAssetAmounts()', () => { it('returns the expected amount for each order', async () => { const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); - const signatures: string[] = _.times(orders.length, i => hexUtils.random()); const expected = orders.map(getDeterministicFillableMakerAssetAmount); const actual = await testContract - .getOrderFillableMakerAssetAmounts(orders, signatures, NULL_ADDRESS) + .getLimitOrderFillableMakerAssetAmounts( + // tslint:disable-next-line:no-unnecessary-type-assertion + orders.map(o => o.order as LimitOrderFields), + orders.map(o => o.signature), + NULL_ADDRESS, + ) .callAsync(); expect(actual).to.deep.eq(expected); }); it('returns empty for no orders', async () => { - const actual = await testContract.getOrderFillableMakerAssetAmounts([], [], NULL_ADDRESS).callAsync(); + const actual = await testContract.getLimitOrderFillableMakerAssetAmounts([], [], NULL_ADDRESS).callAsync(); expect(actual).to.deep.eq([]); }); }); @@ -858,7 +866,9 @@ blockchainTests('erc20-bridge-sampler', env => { } it('can return no quotes', async () => { - const quotes = await testContract.sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], []).callAsync(); + const quotes = await testContract + .sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], []) + .callAsync(); expect(quotes).to.deep.eq([]); }); @@ -866,7 +876,7 @@ blockchainTests('erc20-bridge-sampler', env => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const expectedQuotes = predictSellQuotes([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts); const quotes = await testContract - .sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -876,7 +886,7 @@ blockchainTests('erc20-bridge-sampler', env => { const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); await enableFailTriggerAsync(); const quotes = await testContract - .sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -886,7 +896,11 @@ blockchainTests('erc20-bridge-sampler', env => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const expectedQuotes = predictSellQuotes([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts); const quotes = await testContract - .sampleSellsFromUniswapV2([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts) + .sampleSellsFromUniswapV2( + UNISWAP_V2_ROUTER, + [TAKER_TOKEN, intermediateToken, MAKER_TOKEN], + sampleAmounts, + ) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -898,7 +912,9 @@ blockchainTests('erc20-bridge-sampler', env => { } it('can return no quotes', async () => { - const quotes = await testContract.sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], []).callAsync(); + const quotes = await testContract + .sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], []) + .callAsync(); expect(quotes).to.deep.eq([]); }); @@ -906,7 +922,7 @@ blockchainTests('erc20-bridge-sampler', env => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const expectedQuotes = predictBuyQuotes([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts); const quotes = await testContract - .sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -916,7 +932,7 @@ blockchainTests('erc20-bridge-sampler', env => { const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); await enableFailTriggerAsync(); const quotes = await testContract - .sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -926,7 +942,11 @@ blockchainTests('erc20-bridge-sampler', env => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const expectedQuotes = predictBuyQuotes([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts); const quotes = await testContract - .sampleBuysFromUniswapV2([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts) + .sampleBuysFromUniswapV2( + UNISWAP_V2_ROUTER, + [TAKER_TOKEN, intermediateToken, MAKER_TOKEN], + sampleAmounts, + ) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -935,17 +955,21 @@ blockchainTests('erc20-bridge-sampler', env => { describe('batchCall()', () => { it('can call one function', async () => { const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); - const signatures: string[] = _.times(orders.length, i => hexUtils.random()); const expected = orders.map(getDeterministicFillableTakerAssetAmount); const calls = [ testContract - .getOrderFillableTakerAssetAmounts(orders, signatures, NULL_ADDRESS) + .getLimitOrderFillableTakerAssetAmounts( + // tslint:disable-next-line:no-unnecessary-type-assertion + orders.map(o => o.order as LimitOrderFields), + orders.map(o => o.signature), + NULL_ADDRESS, + ) .getABIEncodedTransactionData(), ]; const r = await testContract.batchCall(calls).callAsync(); expect(r).to.be.length(1); const actual = testContract.getABIDecodedReturnData( - 'getOrderFillableTakerAssetAmounts', + 'getLimitOrderFillableTakerAssetAmounts', r[0].data, ); expect(actual).to.deep.eq(expected); @@ -954,40 +978,53 @@ blockchainTests('erc20-bridge-sampler', env => { it('can call two functions', async () => { const numOrders = _.random(1, 10); const orders = _.times(2, () => createOrders(MAKER_TOKEN, TAKER_TOKEN, numOrders)); - const signatures: string[] = _.times(numOrders, i => hexUtils.random()); const expecteds = [ orders[0].map(getDeterministicFillableTakerAssetAmount), orders[1].map(getDeterministicFillableMakerAssetAmount), ]; const calls = [ testContract - .getOrderFillableTakerAssetAmounts(orders[0], signatures, NULL_ADDRESS) + .getLimitOrderFillableTakerAssetAmounts( + // tslint:disable-next-line:no-unnecessary-type-assertion + orders[0].map(o => o.order as LimitOrderFields), + orders[0].map(o => o.signature), + NULL_ADDRESS, + ) .getABIEncodedTransactionData(), testContract - .getOrderFillableMakerAssetAmounts(orders[1], signatures, NULL_ADDRESS) + .getLimitOrderFillableMakerAssetAmounts( + // tslint:disable-next-line:no-unnecessary-type-assertion + orders[1].map(o => o.order as LimitOrderFields), + orders[1].map(o => o.signature), + NULL_ADDRESS, + ) .getABIEncodedTransactionData(), ]; const r = await testContract.batchCall(calls).callAsync(); expect(r).to.be.length(2); - expect(testContract.getABIDecodedReturnData('getOrderFillableTakerAssetAmounts', r[0].data)).to.deep.eq( - expecteds[0], - ); - expect(testContract.getABIDecodedReturnData('getOrderFillableMakerAssetAmounts', r[1].data)).to.deep.eq( - expecteds[1], - ); + expect( + testContract.getABIDecodedReturnData('getLimitOrderFillableTakerAssetAmounts', r[0].data), + ).to.deep.eq(expecteds[0]); + expect( + testContract.getABIDecodedReturnData('getLimitOrderFillableMakerAssetAmounts', r[1].data), + ).to.deep.eq(expecteds[1]); }); it('can make recursive calls', async () => { const numOrders = _.random(1, 10); const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, numOrders); - const signatures: string[] = _.times(numOrders, i => hexUtils.random()); const expected = orders.map(getDeterministicFillableTakerAssetAmount); let r = await testContract .batchCall([ testContract .batchCall([ testContract - .getOrderFillableTakerAssetAmounts(orders, signatures, NULL_ADDRESS) + .getLimitOrderFillableTakerAssetAmounts( + // tslint:disable-next-line:no-unnecessary-type-assertion + orders.map(o => o.order as LimitOrderFields), + orders.map(o => o.signature), + NULL_ADDRESS, + ) .getABIEncodedTransactionData(), ]) .getABIEncodedTransactionData(), @@ -996,9 +1033,9 @@ blockchainTests('erc20-bridge-sampler', env => { expect(r).to.be.length(1); r = testContract.getABIDecodedReturnData('batchCall', r[0].data); expect(r).to.be.length(1); - expect(testContract.getABIDecodedReturnData('getOrderFillableTakerAssetAmounts', r[0].data)).to.deep.eq( - expected, - ); + expect( + testContract.getABIDecodedReturnData('getLimitOrderFillableTakerAssetAmounts', r[0].data), + ).to.deep.eq(expected); }); }); @@ -1014,12 +1051,12 @@ blockchainTests('erc20-bridge-sampler', env => { const sellAmount = _.last(getSampleAmounts(TAKER_TOKEN))!; const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN]; const uniswapV2FirstHop = testContract - .sampleSellsFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT]) + .sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2FirstHopPath, [constants.ZERO_AMOUNT]) .getABIEncodedTransactionData(); const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN]; const uniswapV2SecondHop = testContract - .sampleSellsFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT]) + .sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2SecondHopPath, [constants.ZERO_AMOUNT]) .getABIEncodedTransactionData(); const eth2DaiFirstHop = testContract @@ -1065,12 +1102,12 @@ blockchainTests('erc20-bridge-sampler', env => { const buyAmount = _.last(getSampleAmounts(MAKER_TOKEN))!; const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN]; const uniswapV2FirstHop = testContract - .sampleBuysFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT]) + .sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2FirstHopPath, [constants.ZERO_AMOUNT]) .getABIEncodedTransactionData(); const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN]; const uniswapV2SecondHop = testContract - .sampleBuysFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT]) + .sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2SecondHopPath, [constants.ZERO_AMOUNT]) .getABIEncodedTransactionData(); const eth2DaiFirstHop = testContract diff --git a/packages/asset-swapper/test/contracts/native_order_sampler_test.ts b/packages/asset-swapper/test/contracts/native_order_sampler_test.ts index 93afd052a3..a2ed5ce87d 100644 --- a/packages/asset-swapper/test/contracts/native_order_sampler_test.ts +++ b/packages/asset-swapper/test/contracts/native_order_sampler_test.ts @@ -7,10 +7,12 @@ import { getRandomInteger, randomAddress, } from '@0x/contracts-test-utils'; -import { Order } from '@0x/types'; +import { SignatureType } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; +import { LimitOrderFields } from '../../src'; +import { NULL_ADDRESS } from '../../src/utils/market_operation_utils/constants'; import { artifacts } from '../artifacts'; import { TestNativeOrderSamplerContract } from '../wrappers'; @@ -18,15 +20,12 @@ const { NULL_BYTES, ZERO_AMOUNT } = constants; // tslint:disable: custom-no-magic-numbers -blockchainTests.resets('NativeOrderSampler contract', env => { +// TODO jacob +blockchainTests.resets.skip('NativeOrderSampler contract', env => { let testContract: TestNativeOrderSamplerContract; let makerToken: string; let takerToken: string; - let feeToken: string; - let erc20Proxy: string; - const ERC20_PROXY_ID = '0xf47261b0'; - const VALID_SIGNATURE = '0x01'; - const INVALID_SIGNATURE = '0x00'; + const VALID_SIGNATURE = { v: 1, r: '0x01', s: '0x01', signatureType: SignatureType.EthSign }; before(async () => { testContract = await TestNativeOrderSamplerContract.deployFrom0xArtifactAsync( @@ -35,9 +34,8 @@ blockchainTests.resets('NativeOrderSampler contract', env => { env.txDefaults, {}, ); - erc20Proxy = await testContract.getAssetProxy(ERC20_PROXY_ID).callAsync(); const NUM_TOKENS = new BigNumber(3); - [makerToken, takerToken, feeToken] = await testContract.createTokens(NUM_TOKENS).callAsync(); + [makerToken, takerToken] = await testContract.createTokens(NUM_TOKENS).callAsync(); await testContract.createTokens(NUM_TOKENS).awaitTransactionSuccessAsync(); }); @@ -51,10 +49,10 @@ blockchainTests.resets('NativeOrderSampler contract', env => { orderTakerAssetFilledAmount: BigNumber; } - function getOrderInfo(order: Order): OrderInfo { + function getOrderInfo(order: LimitOrderFields): OrderInfo { const hash = getPackedHash(hexUtils.leftPad(order.salt)); const orderStatus = order.salt.mod(255).eq(0) ? 3 : 5; - const filledAmount = order.expirationTimeSeconds; + const filledAmount = order.expiry; return { orderStatus, orderHash: hash, @@ -70,61 +68,46 @@ blockchainTests.resets('NativeOrderSampler contract', env => { return new BigNumber(hexUtils.concat(hexUtils.slice(hexUtils.random(), 0, -1), '0xff')); } - function getOrderFillableTakerAmount(order: Order): BigNumber { - return order.takerAssetAmount.minus(getOrderInfo(order).orderTakerAssetFilledAmount); + function getLimitOrderFillableTakerAmount(order: LimitOrderFields): BigNumber { + return order.takerAmount.minus(getOrderInfo(order).orderTakerAssetFilledAmount); } - function getERC20AssetData(tokenAddress: string): string { - return hexUtils.concat(ERC20_PROXY_ID, hexUtils.leftPad(tokenAddress)); - } - - function createOrder(fields: Partial = {}, filledTakerAssetAmount: BigNumber = ZERO_AMOUNT): Order { + function createOrder( + fields: Partial = {}, + filledTakerAssetAmount: BigNumber = ZERO_AMOUNT, + ): LimitOrderFields { return { chainId: 1337, - exchangeAddress: randomAddress(), - makerAddress: randomAddress(), - takerAddress: randomAddress(), - senderAddress: randomAddress(), - feeRecipientAddress: randomAddress(), - makerAssetAmount: getRandomInteger(1e18, 10e18), - takerAssetAmount: getRandomInteger(1e18, 10e18), - makerFee: getRandomInteger(1e18, 10e18), - takerFee: getRandomInteger(1e18, 10e18), - makerAssetData: getERC20AssetData(makerToken), - takerAssetData: getERC20AssetData(takerToken), - makerFeeAssetData: getERC20AssetData(feeToken), - takerFeeAssetData: getERC20AssetData(randomAddress()), + verifyingContract: randomAddress(), + maker: randomAddress(), + taker: randomAddress(), + pool: NULL_BYTES, + sender: NULL_ADDRESS, + feeRecipient: randomAddress(), + makerAmount: getRandomInteger(1, 1e18), + takerAmount: getRandomInteger(1, 1e18), + takerTokenFeeAmount: getRandomInteger(1, 1e18), + makerToken, + takerToken, salt: createFillableOrderSalt(), - // Expiration time will be used to determine filled amount. - expirationTimeSeconds: filledTakerAssetAmount, + expiry: filledTakerAssetAmount, ...fields, }; } async function fundMakerAsync( - order: Order, - assetData: string, + order: LimitOrderFields, balanceScaling: number = 1, allowanceScaling: number = 1, ): Promise { - let token; - let amount; - if (assetData === order.makerAssetData) { - token = makerToken; - amount = - order.makerAssetData === order.makerFeeAssetData - ? order.makerAssetAmount.plus(order.makerFee) - : order.makerAssetAmount; - } else { - token = feeToken; - amount = order.makerFee; - } - amount = amount.times(getOrderFillableTakerAmount(order).div(BigNumber.max(1, order.takerAssetAmount))); + const token = makerToken; + let amount = order.makerAmount; + amount = amount.times(getLimitOrderFillableTakerAmount(order).div(BigNumber.max(1, order.takerAmount))); await testContract .setTokenBalanceAndAllowance( token, - order.makerAddress, - erc20Proxy, + order.maker, + testContract.address, amount.times(balanceScaling).integerValue(), amount.times(allowanceScaling).integerValue(), ) @@ -132,7 +115,7 @@ blockchainTests.resets('NativeOrderSampler contract', env => { } describe('getTokenDecimals()', () => { - it('correctly returns the token balances', async () => { + it('correctly returns the token decimals', async () => { const newMakerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync( erc20Artifacts.DummyERC20Token, env.provider, @@ -154,130 +137,44 @@ blockchainTests.resets('NativeOrderSampler contract', env => { constants.DUMMY_TOKEN_TOTAL_SUPPLY, ); const [makerDecimals, takerDecimals] = await testContract - .getTokenDecimals(newMakerToken.address, newTakerToken.address) + .getTokenDecimals([newMakerToken.address, newTakerToken.address]) .callAsync(); expect(makerDecimals.toString()).to.eql('18'); expect(takerDecimals.toString()).to.eql('6'); }); }); - describe('getOrderFillableTakerAmount()', () => { + describe('getLimitOrderFillableTakerAmount()', () => { it('returns the full amount for a fully funded order', async () => { const order = createOrder(); - const expected = getOrderFillableTakerAmount(order); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); + const expected = getLimitOrderFillableTakerAmount(order); + await fundMakerAsync(order); const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - expect(actual).to.bignumber.eq(expected); - }); - - it('returns the full amount for a fully funded order without maker fees', async () => { - const order = createOrder({ makerFee: ZERO_AMOUNT }); - const expected = getOrderFillableTakerAmount(order); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - expect(actual).to.bignumber.eq(expected); - }); - - it('returns the full amount for a fully funded order without maker fee asset data', async () => { - const order = createOrder({ makerFeeAssetData: NULL_BYTES }); - const expected = getOrderFillableTakerAmount(order); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - expect(actual).to.bignumber.eq(expected); - }); - - it('returns the full amount for a fully funded order with maker fees denominated in the maker asset', async () => { - const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) }); - const expected = getOrderFillableTakerAmount(order); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) .callAsync(); expect(actual).to.bignumber.eq(expected); }); it('returns partial amount with insufficient maker asset balance', async () => { const order = createOrder(); - const expected = getOrderFillableTakerAmount(order) + const expected = getLimitOrderFillableTakerAmount(order) .times(0.5) .integerValue(BigNumber.ROUND_DOWN); - await fundMakerAsync(order, order.makerAssetData, 0.5); - await fundMakerAsync(order, order.makerFeeAssetData); + await fundMakerAsync(order, 0.5); const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) .callAsync(); assertIntegerRoughlyEquals(actual, expected, 100); }); it('returns partial amount with insufficient maker asset allowance', async () => { const order = createOrder(); - const expected = getOrderFillableTakerAmount(order) + const expected = getLimitOrderFillableTakerAmount(order) .times(0.5) .integerValue(BigNumber.ROUND_DOWN); - await fundMakerAsync(order, order.makerAssetData, 1, 0.5); - await fundMakerAsync(order, order.makerFeeAssetData); + await fundMakerAsync(order, 1, 0.5); const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - assertIntegerRoughlyEquals(actual, expected, 100); - }); - - it('returns partial amount with insufficient maker fee asset balance', async () => { - const order = createOrder(); - const expected = getOrderFillableTakerAmount(order) - .times(0.5) - .integerValue(BigNumber.ROUND_DOWN); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData, 0.5); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - assertIntegerRoughlyEquals(actual, expected, 100); - }); - - it('returns partial amount with insufficient maker fee asset allowance', async () => { - const order = createOrder(); - const expected = getOrderFillableTakerAmount(order) - .times(0.5) - .integerValue(BigNumber.ROUND_DOWN); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData, 1, 0.5); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - assertIntegerRoughlyEquals(actual, expected, 100); - }); - - it('returns partial amount with insufficient maker asset balance (maker asset fees)', async () => { - const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) }); - const expected = getOrderFillableTakerAmount(order) - .times(0.5) - .integerValue(BigNumber.ROUND_DOWN); - await fundMakerAsync(order, order.makerAssetData, 0.5); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) - .callAsync(); - assertIntegerRoughlyEquals(actual, expected, 100); - }); - - it('returns partial amount with insufficient maker asset allowance (maker asset fees)', async () => { - const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) }); - const expected = getOrderFillableTakerAmount(order) - .times(0.5) - .integerValue(BigNumber.ROUND_DOWN); - await fundMakerAsync(order, order.makerAssetData, 1, 0.5); - const actual = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) .callAsync(); assertIntegerRoughlyEquals(actual, expected, 100); }); @@ -287,10 +184,9 @@ blockchainTests.resets('NativeOrderSampler contract', env => { ...createOrder(), salt: createUnfillableOrderSalt(), }; - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); + await fundMakerAsync(order); const fillableTakerAmount = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) .callAsync(); expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT); }); @@ -298,12 +194,11 @@ blockchainTests.resets('NativeOrderSampler contract', env => { it('returns zero for an order with zero maker asset amount', async () => { const order = { ...createOrder(), - makerAssetAmount: ZERO_AMOUNT, + makerAmount: ZERO_AMOUNT, }; - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); + await fundMakerAsync(order); const fillableTakerAmount = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) .callAsync(); expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT); }); @@ -311,32 +206,24 @@ blockchainTests.resets('NativeOrderSampler contract', env => { it('returns zero for an order with zero taker asset amount', async () => { const order = { ...createOrder(), - takerAssetAmount: ZERO_AMOUNT, + takerAmount: ZERO_AMOUNT, }; - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); + await fundMakerAsync(order); const fillableTakerAmount = await testContract - .getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address) .callAsync(); expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT); }); it('returns zero for an order with an empty signature', async () => { const order = createOrder(); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); + await fundMakerAsync(order); const fillableTakerAmount = await testContract - .getOrderFillableTakerAmount(order, NULL_BYTES, testContract.address) - .callAsync(); - expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT); - }); - - it('returns zero for an order with an invalid signature', async () => { - const order = createOrder(); - await fundMakerAsync(order, order.makerAssetData); - await fundMakerAsync(order, order.makerFeeAssetData); - const fillableTakerAmount = await testContract - .getOrderFillableTakerAmount(order, INVALID_SIGNATURE, testContract.address) + .getLimitOrderFillableTakerAmount( + order, + { ...VALID_SIGNATURE, r: NULL_BYTES, s: NULL_BYTES }, + testContract.address, + ) .callAsync(); expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT); }); diff --git a/packages/asset-swapper/test/dex_sampler_test.ts b/packages/asset-swapper/test/dex_sampler_test.ts index b9de60c9bd..892387cbff 100644 --- a/packages/asset-swapper/test/dex_sampler_test.ts +++ b/packages/asset-swapper/test/dex_sampler_test.ts @@ -7,28 +7,28 @@ import { randomAddress, toBaseUnitAmount, } from '@0x/contracts-test-utils'; -import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; -import { BigNumber, hexUtils } from '@0x/utils'; +import { FillQuoteTransformerOrderType, LimitOrderFields, SignatureType } from '@0x/protocol-utils'; +import { BigNumber, hexUtils, NULL_ADDRESS, NULL_BYTES } from '@0x/utils'; +import { Pool } from '@balancer-labs/sor/dist/types'; import * as _ from 'lodash'; -import { BalancerPool } from '../src/utils/market_operation_utils/balancer_utils'; +import { SignedOrder } from '../src/types'; import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types'; import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache'; import { MockSamplerContract } from './utils/mock_sampler_contract'; +import { generatePseudoRandomSalt } from './utils/utils'; const CHAIN_ID = 1; // tslint:disable: custom-no-magic-numbers describe('DexSampler tests', () => { const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); - const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); - const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken; const exchangeAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchange; + const exchangeProxyAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchangeProxy; const tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [wethAddress] }; @@ -67,36 +67,39 @@ describe('DexSampler tests', () => { }); }); - function createOrder(overrides?: Partial): SignedOrder { - return { - chainId: CHAIN_ID, - exchangeAddress: randomAddress(), - makerAddress: constants.NULL_ADDRESS, - takerAddress: constants.NULL_ADDRESS, - senderAddress: constants.NULL_ADDRESS, - feeRecipientAddress: randomAddress(), - salt: generatePseudoRandomSalt(), - expirationTimeSeconds: getRandomInteger(0, 2 ** 64), - makerAssetData: MAKER_ASSET_DATA, - takerAssetData: TAKER_ASSET_DATA, - makerFeeAssetData: constants.NULL_BYTES, - takerFeeAssetData: constants.NULL_BYTES, - makerAssetAmount: getRandomInteger(1, 1e18), - takerAssetAmount: getRandomInteger(1, 1e18), - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - signature: hexUtils.random(), - ...overrides, + function createOrder(overrides?: Partial): SignedOrder { + const o: SignedOrder = { + order: { + salt: generatePseudoRandomSalt(), + expiry: getRandomInteger(0, 2 ** 64), + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + makerAmount: getRandomInteger(1, 1e18), + takerAmount: getRandomInteger(1, 1e18), + takerTokenFeeAmount: constants.ZERO_AMOUNT, + chainId: CHAIN_ID, + pool: NULL_BYTES, + feeRecipient: NULL_ADDRESS, + sender: NULL_ADDRESS, + maker: NULL_ADDRESS, + taker: NULL_ADDRESS, + verifyingContract: exchangeProxyAddress, + ...overrides, + }, + signature: { v: 1, r: hexUtils.random(), s: hexUtils.random(), signatureType: SignatureType.EthSign }, + type: FillQuoteTransformerOrderType.Limit, }; + return o; } const ORDERS = _.times(4, () => createOrder()); - const SIMPLE_ORDERS = ORDERS.map(o => _.omit(o, ['signature', 'chainId', 'exchangeAddress'])); + const SIMPLE_ORDERS = ORDERS.map(o => _.omit(o, ['signature', 'chainId'])); describe('operations', () => { - it('getOrderFillableMakerAmounts()', async () => { + // TODO jacob + it.skip('getLimitOrderFillableMakerAssetAmounts()', async () => { const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); const sampler = new MockSamplerContract({ - getOrderFillableMakerAssetAmounts: (orders, signatures) => { + getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => { expect(orders).to.deep.eq(SIMPLE_ORDERS); expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); return expectedFillableAmounts; @@ -112,15 +115,16 @@ describe('DexSampler tests', () => { async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( - dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeAddress), ); expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); }); - it('getOrderFillableTakerAmounts()', async () => { + // TODO jacob + it.skip('getLimitOrderFillableTakerAssetAmounts()', async () => { const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); const sampler = new MockSamplerContract({ - getOrderFillableTakerAssetAmounts: (orders, signatures) => { + getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => { expect(orders).to.deep.eq(SIMPLE_ORDERS); expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); return expectedFillableAmounts; @@ -136,7 +140,7 @@ describe('DexSampler tests', () => { async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( - dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeAddress), ); expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); }); @@ -324,7 +328,7 @@ describe('DexSampler tests', () => { const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); const sampler = new MockSamplerContract({ - sampleSellsFromUniswapV2: (path, fillAmounts) => { + sampleSellsFromUniswapV2: (_router, path, fillAmounts) => { expect(path).to.deep.eq([expectedMakerToken, expectedTakerToken]); expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); return expectedMakerFillAmounts; @@ -419,6 +423,7 @@ describe('DexSampler tests', () => { [ERC20BridgeSource.UniswapV2]: getRandomFloat(0, 100), }; const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); + let uniswapRouter: string; const sampler = new MockSamplerContract({ sampleSellsFromUniswap: (takerToken, makerToken, fillAmounts) => { expect(takerToken).to.eq(expectedTakerToken); @@ -432,7 +437,8 @@ describe('DexSampler tests', () => { expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); }, - sampleSellsFromUniswapV2: (path, fillAmounts) => { + sampleSellsFromUniswapV2: (router, path, fillAmounts) => { + uniswapRouter = router; if (path.length === 2) { expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); } else if (path.length === 3) { @@ -468,7 +474,7 @@ describe('DexSampler tests', () => { output: a.times(ratesBySource[s]).integerValue(), fillData: s === ERC20BridgeSource.UniswapV2 - ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } + ? { router: uniswapRouter, tokenAddressPath: [expectedTakerToken, expectedMakerToken] } : {}, })), ); @@ -478,6 +484,7 @@ describe('DexSampler tests', () => { input: a, output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(), fillData: { + router: uniswapRouter, tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken], }, })), @@ -494,7 +501,7 @@ describe('DexSampler tests', () => { const expectedTakerToken = randomAddress(); const expectedMakerToken = randomAddress(); const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); - const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()]; + const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()]; const balancerPoolsCache = new MockBalancerPoolsCache({ getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { expect(takerToken).equal(expectedTakerToken); @@ -528,6 +535,7 @@ describe('DexSampler tests', () => { [ERC20BridgeSource.UniswapV2]: getRandomFloat(0, 100), }; const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); + let uniswapRouter: string; const sampler = new MockSamplerContract({ sampleBuysFromUniswap: (takerToken, makerToken, fillAmounts) => { expect(takerToken).to.eq(expectedTakerToken); @@ -541,7 +549,8 @@ describe('DexSampler tests', () => { expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); }, - sampleBuysFromUniswapV2: (path, fillAmounts) => { + sampleBuysFromUniswapV2: (router, path, fillAmounts) => { + uniswapRouter = router; if (path.length === 2) { expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); } else if (path.length === 3) { @@ -572,7 +581,7 @@ describe('DexSampler tests', () => { output: a.times(ratesBySource[s]).integerValue(), fillData: s === ERC20BridgeSource.UniswapV2 - ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } + ? { router: uniswapRouter, tokenAddressPath: [expectedTakerToken, expectedMakerToken] } : {}, })), ); @@ -582,6 +591,7 @@ describe('DexSampler tests', () => { input: a, output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(), fillData: { + router: uniswapRouter, tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken], }, })), @@ -594,7 +604,7 @@ describe('DexSampler tests', () => { const expectedTakerToken = randomAddress(); const expectedMakerToken = randomAddress(); const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); - const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()]; + const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()]; const balancerPoolsCache = new MockBalancerPoolsCache({ getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { expect(takerToken).equal(expectedTakerToken); @@ -621,16 +631,16 @@ describe('DexSampler tests', () => { }); describe('batched operations', () => { - it('getOrderFillableMakerAmounts(), getOrderFillableTakerAmounts()', async () => { + it.skip('getLimitOrderFillableMakerAssetAmounts(), getLimitOrderFillableTakerAssetAmounts()', async () => { const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); const sampler = new MockSamplerContract({ - getOrderFillableMakerAssetAmounts: (orders, signatures) => { + getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => { expect(orders).to.deep.eq(SIMPLE_ORDERS); expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); return expectedFillableMakerAmounts; }, - getOrderFillableTakerAssetAmounts: (orders, signatures) => { + getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => { expect(orders).to.deep.eq(SIMPLE_ORDERS); expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); return expectedFillableTakerAmounts; @@ -646,15 +656,15 @@ describe('DexSampler tests', () => { async () => undefined, ); const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync( - dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), - dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeAddress), ); expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts); expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts); }); }); }); -function generateBalancerPool(): BalancerPool { +function generateBalancerPool(): Pool { return { id: randomAddress(), balanceIn: getRandomInteger(1, 1e18), diff --git a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts index d8881c62fc..120cca401a 100644 --- a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts @@ -1,16 +1,17 @@ import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; -import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils'; +import { constants as contractConstants, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; import { - assetDataUtils, decodeAffiliateFeeTransformerData, decodeFillQuoteTransformerData, decodePayTakerTransformerData, decodeWethTransformerData, ETH_TOKEN_ADDRESS, + FillQuoteTransformerLimitOrderInfo, + FillQuoteTransformerOrderType, FillQuoteTransformerSide, getTransformerAddress, -} from '@0x/order-utils'; -import { Order } from '@0x/types'; + LimitOrderFields, +} from '@0x/protocol-utils'; import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; @@ -18,11 +19,15 @@ import 'mocha'; import { constants } from '../src/constants'; import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer'; -import { getSwapMinBuyAmount } from '../src/quote_consumers/utils'; import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; -import { ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types'; +import { + ERC20BridgeSource, + OptimizedLimitOrder, + OptimizedMarketOrder, +} from '../src/utils/market_operation_utils/types'; import { chaiSetup } from './utils/chai_setup'; +import { getRandomAmount, getRandomSignature } from './utils/utils'; chaiSetup.configure(); const expect = chai.expect; @@ -61,76 +66,91 @@ describe('ExchangeProxySwapQuoteConsumer', () => { consumer = new ExchangeProxySwapQuoteConsumer(fakeProvider, contractAddresses, { chainId: CHAIN_ID }); }); - function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber { - return getRandomInteger(1, maxAmount); - } - - function createAssetData(token?: string): string { - return assetDataUtils.encodeERC20AssetData(token || randomAddress()); - } - - function getRandomOrder(): OptimizedMarketOrder { + function getRandomOrder(orderFields?: Partial): LimitOrderFields { return { - fillableMakerAssetAmount: getRandomAmount(), - fillableTakerFeeAmount: getRandomAmount(), - fillableTakerAssetAmount: getRandomAmount(), - fills: [], chainId: CHAIN_ID, - exchangeAddress: contractAddresses.exchange, - expirationTimeSeconds: getRandomInteger(1, 2e9), - feeRecipientAddress: randomAddress(), - makerAddress: randomAddress(), - makerAssetAmount: getRandomAmount(), - takerAssetAmount: getRandomAmount(), - makerFee: getRandomAmount(), - takerFee: getRandomAmount(), + verifyingContract: contractAddresses.exchangeProxy, + expiry: getRandomInteger(1, 2e9), + feeRecipient: randomAddress(), + sender: randomAddress(), + pool: hexUtils.random(32), + maker: randomAddress(), + makerAmount: getRandomAmount(), + takerAmount: getRandomAmount(), + takerTokenFeeAmount: getRandomAmount(), salt: getRandomAmount(2e9), - signature: hexUtils.random(66), - senderAddress: NULL_ADDRESS, - takerAddress: NULL_ADDRESS, - makerAssetData: createAssetData(MAKER_TOKEN), - takerAssetData: createAssetData(TAKER_TOKEN), - makerFeeAssetData: createAssetData(), - takerFeeAssetData: createAssetData(), + taker: NULL_ADDRESS, + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + ...orderFields, + }; + } + + function getRandomOptimizedMarketOrder( + optimizerFields?: Partial, + orderFields?: Partial, + ): OptimizedLimitOrder { + const order = getRandomOrder(orderFields); + return { + source: ERC20BridgeSource.Native, + fillData: { + order, + signature: getRandomSignature(), + maxTakerTokenFillAmount: order.takerAmount, + }, + type: FillQuoteTransformerOrderType.Limit, + makerToken: order.makerToken, + takerToken: order.takerToken, + makerAmount: order.makerAmount, + takerAmount: order.takerAmount, + fills: [], + ...optimizerFields, }; } function getRandomQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote { + const order = getRandomOptimizedMarketOrder(); + const makerTokenFillAmount = order.makerAmount; + const takerTokenFillAmount = order.takerAmount; return { gasPrice: getRandomInteger(1, 1e9), - type: side, - makerAssetData: createAssetData(MAKER_TOKEN), - takerAssetData: createAssetData(TAKER_TOKEN), - orders: [getRandomOrder()], + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + orders: [order], + makerTokenDecimals: 18, + takerTokenDecimals: 18, + sourceBreakdown: {} as any, + isTwoHop: false, bestCaseQuoteInfo: { - feeTakerAssetAmount: getRandomAmount(), - makerAssetAmount: getRandomAmount(), + makerAmount: makerTokenFillAmount, + takerAmount: takerTokenFillAmount, + totalTakerAmount: takerTokenFillAmount, gas: Math.floor(Math.random() * 8e6), protocolFeeInWeiAmount: getRandomAmount(), - takerAssetAmount: getRandomAmount(), - totalTakerAssetAmount: getRandomAmount(), + feeTakerTokenAmount: getRandomAmount(), }, worstCaseQuoteInfo: { - feeTakerAssetAmount: getRandomAmount(), - makerAssetAmount: getRandomAmount(), + makerAmount: makerTokenFillAmount, + takerAmount: takerTokenFillAmount, + totalTakerAmount: takerTokenFillAmount, gas: Math.floor(Math.random() * 8e6), protocolFeeInWeiAmount: getRandomAmount(), - takerAssetAmount: getRandomAmount(), - totalTakerAssetAmount: getRandomAmount(), + feeTakerTokenAmount: getRandomAmount(), }, ...(side === MarketOperation.Buy - ? { makerAssetFillAmount: getRandomAmount() } - : { takerAssetFillAmount: getRandomAmount() }), - } as any; + ? { type: MarketOperation.Buy, makerTokenFillAmount } + : { type: MarketOperation.Sell, takerTokenFillAmount }), + takerTokenToEthRate: getRandomAmount(), + makerTokenToEthRate: getRandomAmount(), + }; } function getRandomTwoHopQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote { - const intermediateTokenAssetData = createAssetData(INTERMEDIATE_TOKEN); return { ...getRandomQuote(side), orders: [ - { ...getRandomOrder(), makerAssetData: intermediateTokenAssetData }, - { ...getRandomOrder(), takerAssetData: intermediateTokenAssetData }, + getRandomOptimizedMarketOrder({ makerToken: INTERMEDIATE_TOKEN }, { makerToken: INTERMEDIATE_TOKEN }), + getRandomOptimizedMarketOrder({ takerToken: INTERMEDIATE_TOKEN }, { takerToken: INTERMEDIATE_TOKEN }), ], isTwoHop: true, } as any; @@ -144,20 +164,28 @@ describe('ExchangeProxySwapQuoteConsumer', () => { return getRandomQuote(MarketOperation.Buy) as MarketBuySwapQuote; } - type PlainOrder = Exclude; + type PlainOrder = Exclude; function cleanOrders(orders: OptimizedMarketOrder[]): PlainOrder[] { return orders.map( o => - _.omit(o, [ - 'chainId', - 'exchangeAddress', - 'fillableMakerAssetAmount', - 'fillableTakerAssetAmount', - 'fillableTakerFeeAmount', - 'fills', - 'signature', - ]) as PlainOrder, + _.omit( + { + ...o.fillData, + order: _.omit((o.fillData as FillQuoteTransformerLimitOrderInfo).order, [ + 'chainId', + 'verifyingContract', + ]) as any, + }, + [ + 'fillableMakerAssetAmount', + 'fillableTakerAssetAmount', + 'fillableTakerFeeAmount', + 'fills', + 'chainId', + 'verifyingContract', + ], + ) as PlainOrder, ); } @@ -184,25 +212,25 @@ describe('ExchangeProxySwapQuoteConsumer', () => { }>; } - const liquidityProviderEncoder = AbiEncoder.createMethod('sellToLiquidityProvider', [ - { type: 'address', name: 'inputToken' }, - { type: 'address', name: 'outputToken' }, - { type: 'address', name: 'target' }, - { type: 'address', name: 'recipient' }, - { type: 'uint256', name: 'sellAmount' }, - { type: 'uint256', name: 'minBuyAmount' }, - { type: 'bytes', name: 'auxiliaryData' }, - ]); + // const liquidityProviderEncoder = AbiEncoder.createMethod('sellToLiquidityProvider', [ + // { type: 'address', name: 'inputToken' }, + // { type: 'address', name: 'outputToken' }, + // { type: 'address', name: 'target' }, + // { type: 'address', name: 'recipient' }, + // { type: 'uint256', name: 'sellAmount' }, + // { type: 'uint256', name: 'minBuyAmount' }, + // { type: 'bytes', name: 'auxiliaryData' }, + // ]); - interface LiquidityProviderArgs { - inputToken: string; - outputToken: string; - target: string; - recipient: string; - sellAmount: BigNumber; - minBuyAmount: BigNumber; - auxiliaryData: string; - } + // interface LiquidityProviderArgs { + // inputToken: string; + // outputToken: string; + // target: string; + // recipient: string; + // sellAmount: BigNumber; + // minBuyAmount: BigNumber; + // auxiliaryData: string; + // } describe('getCalldataOrThrow()', () => { it('can produce a sell quote', async () => { @@ -211,8 +239,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => { const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args; expect(callArgs.inputToken).to.eq(TAKER_TOKEN); expect(callArgs.outputToken).to.eq(MAKER_TOKEN); - expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); - expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote)); + expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount); expect(callArgs.transformations).to.be.length(2); expect( callArgs.transformations[0].deploymentNonce.toNumber() === @@ -224,9 +252,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => { ); const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); - expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount); - expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders)); - expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature)); + expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerTokenFillAmount); + expect(fillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders(quote.orders)); + expect(fillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq( + (quote.orders as OptimizedLimitOrder[]).map(o => o.fillData.signature), + ); expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data); @@ -240,8 +270,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => { const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args; expect(callArgs.inputToken).to.eq(TAKER_TOKEN); expect(callArgs.outputToken).to.eq(MAKER_TOKEN); - expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); - expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote)); + expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount); expect(callArgs.transformations).to.be.length(2); expect( callArgs.transformations[0].deploymentNonce.toNumber() === @@ -253,9 +283,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => { ); const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy); - expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount); - expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders)); - expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature)); + expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerTokenFillAmount); + expect(fillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders(quote.orders)); + expect(fillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq( + (quote.orders as OptimizedLimitOrder[]).map(o => o.fillData.signature), + ); expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data); @@ -281,7 +313,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { consumer.transformerNonces.wethTransformer, ); const wethTransformerData = decodeWethTransformerData(callArgs.transformations[0].data); - expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); + expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount); expect(wethTransformerData.token).to.eq(ETH_TOKEN_ADDRESS); }); @@ -338,8 +370,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => { const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args; expect(callArgs.inputToken).to.eq(TAKER_TOKEN); expect(callArgs.outputToken).to.eq(MAKER_TOKEN); - expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); - expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote)); + expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount); expect(callArgs.transformations).to.be.length(3); expect( callArgs.transformations[0].deploymentNonce.toNumber() === @@ -356,16 +388,20 @@ describe('ExchangeProxySwapQuoteConsumer', () => { const [firstHopOrder, secondHopOrder] = quote.orders; const firstHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); expect(firstHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); - expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAssetAmount); - expect(firstHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([firstHopOrder])); - expect(firstHopFillQuoteTransformerData.signatures).to.deep.eq([firstHopOrder.signature]); + expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAmount); + expect(firstHopFillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders([firstHopOrder])); + expect(firstHopFillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq([ + (firstHopOrder as OptimizedLimitOrder).fillData.signature, + ]); expect(firstHopFillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); expect(firstHopFillQuoteTransformerData.buyToken).to.eq(INTERMEDIATE_TOKEN); const secondHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[1].data); expect(secondHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); expect(secondHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(contractConstants.MAX_UINT256); - expect(secondHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([secondHopOrder])); - expect(secondHopFillQuoteTransformerData.signatures).to.deep.eq([secondHopOrder.signature]); + expect(secondHopFillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders([secondHopOrder])); + expect(secondHopFillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq([ + (secondHopOrder as OptimizedLimitOrder).fillData.signature, + ]); expect(secondHopFillQuoteTransformerData.sellToken).to.eq(INTERMEDIATE_TOKEN); expect(secondHopFillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[2].data); @@ -377,36 +413,36 @@ describe('ExchangeProxySwapQuoteConsumer', () => { INTERMEDIATE_TOKEN, ]); }); - it.skip('Uses the `LiquidityProviderFeature` if given a single LiquidityProvider order', async () => { - const quote = { - ...getRandomSellQuote(), - orders: [ - { - ...getRandomOrder(), - fills: [ - { - source: ERC20BridgeSource.LiquidityProvider, - sourcePathId: '', - input: constants.ZERO_AMOUNT, - output: constants.ZERO_AMOUNT, - subFills: [], - }, - ], - }, - ], - }; - const callInfo = await consumer.getCalldataOrThrowAsync(quote); - const callArgs = liquidityProviderEncoder.decode(callInfo.calldataHexString) as LiquidityProviderArgs; - expect(callArgs).to.deep.equal({ - inputToken: TAKER_TOKEN, - outputToken: MAKER_TOKEN, - target: quote.orders[0].makerAddress, - recipient: constants.NULL_ADDRESS, - sellAmount: quote.worstCaseQuoteInfo.totalTakerAssetAmount, - minBuyAmount: getSwapMinBuyAmount(quote), - auxiliaryData: constants.NULL_BYTES, - }); - }); + // it.skip('Uses the `LiquidityProviderFeature` if given a single LiquidityProvider order', async () => { + // const quote = { + // ...getRandomSellQuote(), + // orders: [ + // { + // ...getRandomOrder(), + // fills: [ + // { + // source: ERC20BridgeSource.LiquidityProvider, + // sourcePathId: '', + // input: constants.ZERO_AMOUNT, + // output: constants.ZERO_AMOUNT, + // subFills: [], + // }, + // ], + // }, + // ], + // }; + // const callInfo = await consumer.getCalldataOrThrowAsync(quote); + // const callArgs = liquidityProviderEncoder.decode(callInfo.calldataHexString) as LiquidityProviderArgs; + // expect(callArgs).to.deep.equal({ + // inputToken: TAKER_TOKEN, + // outputToken: MAKER_TOKEN, + // target: quote.orders[0].makerAddress, + // recipient: constants.NULL_ADDRESS, + // sellAmount: quote.worstCaseQuoteInfo.feeTakerTokenAmount, + // minBuyAmount: getSwapMinBuyAmount(quote), + // auxiliaryData: constants.NULL_BYTES, + // }); + // }); it('allows selling the entire balance for CFL', async () => { const quote = getRandomSellQuote(); const callInfo = await consumer.getCalldataOrThrowAsync(quote, { @@ -416,7 +452,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { expect(callArgs.inputToken).to.eq(TAKER_TOKEN); expect(callArgs.outputToken).to.eq(MAKER_TOKEN); expect(callArgs.inputTokenAmount).to.bignumber.eq(MAX_UINT256); - expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote)); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount); expect(callArgs.transformations).to.be.length(2); expect( callArgs.transformations[0].deploymentNonce.toNumber() === @@ -429,8 +465,10 @@ describe('ExchangeProxySwapQuoteConsumer', () => { const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(MAX_UINT256); - expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders)); - expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature)); + expect(fillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders(quote.orders)); + expect(fillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq( + (quote.orders as OptimizedLimitOrder[]).map(o => o.fillData.signature), + ); expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data); diff --git a/packages/asset-swapper/test/fillable_amounts_utils_test.ts b/packages/asset-swapper/test/fillable_amounts_utils_test.ts deleted file mode 100644 index 85e6876187..0000000000 --- a/packages/asset-swapper/test/fillable_amounts_utils_test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as chai from 'chai'; -import * as _ from 'lodash'; -import 'mocha'; - -import { fillableAmountsUtils } from '../src/utils/fillable_amounts_utils'; - -import { chaiSetup } from './utils/chai_setup'; -import { testOrderFactory } from './utils/test_order_factory'; -import { baseUnitAmount } from './utils/utils'; - -chaiSetup.configure(); -const expect = chai.expect; - -// tslint:disable:custom-no-magic-numbers -const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b02222222222222222222222222222222222222222222222222222222222222222'; -const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b01111111111111111111111111111111111111111111111111111111111111111'; - -const TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSignedOrderWithFillableAmounts({ - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - takerFee: baseUnitAmount(2), - fillableMakerAssetAmount: baseUnitAmount(5), - fillableTakerAssetAmount: baseUnitAmount(10), - fillableTakerFeeAmount: baseUnitAmount(2), -}); - -const MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSignedOrderWithFillableAmounts({ - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - takerFee: baseUnitAmount(2), - fillableMakerAssetAmount: baseUnitAmount(10), - fillableTakerAssetAmount: baseUnitAmount(5), - fillableTakerFeeAmount: baseUnitAmount(2), -}); - -describe('fillableAmountsUtils', () => { - describe('getTakerAssetAmountSwappedAfterOrderFees', () => { - it('should return fillableTakerAssetAmount if takerFee is not denominated in taker', () => { - const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees( - MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, - ); - expect(availableAssetAmount).to.bignumber.eq( - MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER.fillableTakerAssetAmount, - ); - }); - - it('should return fillableTakerAssetAmount + fillableTakerFeeAmount if takerFee is not denominated in maker', () => { - const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees( - TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, - ); - expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(12)); - }); - }); - describe('getMakerAssetAmountSwappedAfterOrderFees', () => { - it('should return fillableMakerAssetAmount if takerFee is not denominated in maker', () => { - const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees( - TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, - ); - expect(availableAssetAmount).to.bignumber.eq( - TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER.fillableMakerAssetAmount, - ); - }); - - it('should return fillableMakerAssetAmount - fillableTakerFeeif takerFee is denominated in maker', () => { - const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees( - MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, - ); - expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(8)); - }); - }); -}); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 28f49348e8..0f318e1b25 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -5,20 +5,18 @@ import { constants, expect, getRandomFloat, - getRandomInteger, Numberish, randomAddress, } from '@0x/contracts-test-utils'; -import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; -import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types'; -import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; +import { FillQuoteTransformerOrderType, LimitOrder, RfqOrder, SignatureType } from '@0x/protocol-utils'; +import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; import * as TypeMoq from 'typemoq'; -import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; -import { PriceAwareRFQFlags } from '../src/types'; -import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; +import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedNativeOrder } from '../src'; +import { NativeOrderWithFillableAmounts } from '../src/types'; +import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils'; import { BRIDGE_ADDRESSES_BY_CHAIN, @@ -26,7 +24,6 @@ import { POSITIVE_INF, SELL_SOURCE_FILTER, SOURCE_FLAGS, - ZERO_AMOUNT, } from '../src/utils/market_operation_utils/constants'; import { CreamPoolsCache } from '../src/utils/market_operation_utils/cream_utils'; import { createFills } from '../src/utils/market_operation_utils/fills'; @@ -40,15 +37,17 @@ import { FillData, GenerateOptimizedOrdersOpts, GetMarketOrdersOpts, + LiquidityProviderFillData, MarketSideLiquidity, NativeFillData, + OptimizedMarketBridgeOrder, + OptimizerResultWithReport, TokenAdjacencyGraph, } from '../src/utils/market_operation_utils/types'; const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); -const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); -const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); + const DEFAULT_EXCLUDED = [ ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Curve, @@ -69,10 +68,42 @@ const DEFAULT_EXCLUDED = [ const BUY_SOURCES = BUY_SOURCE_FILTER.sources; const SELL_SOURCES = SELL_SOURCE_FILTER.sources; const TOKEN_ADJACENCY_GRAPH: TokenAdjacencyGraph = { default: [] }; -const PRICE_AWARE_RFQ_ENABLED: PriceAwareRFQFlags = { - isFirmPriceAwareEnabled: true, - isIndicativePriceAwareEnabled: true, -}; + +const SIGNATURE = { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign }; + +/** + * gets the orders required for a market sell operation by (potentially) merging native orders with + * generated bridge orders. + * @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders + * @param takerAmount Amount of taker asset to sell. + * @param opts Options object. + * @return object with optimized orders and a QuoteReport + */ +async function getMarketSellOrdersAsync( + utils: MarketOperationUtils, + nativeOrders: SignedNativeOrder[], + takerAmount: BigNumber, + opts?: Partial, +): Promise { + return utils.getOptimizerResultAsync(nativeOrders, takerAmount, MarketOperation.Sell, opts); +} + +/** + * gets the orders required for a market buy operation by (potentially) merging native orders with + * generated bridge orders. + * @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders + * @param makerAmount Amount of maker asset to buy. + * @param opts Options object. + * @return object with optimized orders and a QuoteReport + */ +async function getMarketBuyOrdersAsync( + utils: MarketOperationUtils, + nativeOrders: SignedNativeOrder[], + makerAmount: BigNumber, + opts?: Partial, +): Promise { + return utils.getOptimizerResultAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts); +} // tslint:disable: custom-no-magic-numbers promise-function-async describe('MarketOperationUtils tests', () => { @@ -84,7 +115,7 @@ describe('MarketOperationUtils tests', () => { function getMockedQuoteRequestor( type: 'indicative' | 'firm', - results: SignedOrder[], + results: SignedNativeOrder[], verifiable: TypeMoq.Times, ): TypeMoq.IMock { const args: [any, any, any, any, any, any] = [ @@ -99,98 +130,49 @@ describe('MarketOperationUtils tests', () => { if (type === 'firm') { requestor .setup(r => r.requestRfqtFirmQuotesAsync(...args)) - .returns(async () => results.map(result => ({ signedOrder: result }))) + .returns(async () => results) .verifiable(verifiable); } else { requestor .setup(r => r.requestRfqtIndicativeQuotesAsync(...args)) - .returns(async () => results) + .returns(async () => results.map(r => r.order)) .verifiable(verifiable); } return requestor; } - function createOrder(overrides?: Partial): SignedOrder { - return { - chainId: CHAIN_ID, - exchangeAddress: contractAddresses.exchange, - makerAddress: constants.NULL_ADDRESS, - takerAddress: constants.NULL_ADDRESS, - senderAddress: constants.NULL_ADDRESS, - feeRecipientAddress: randomAddress(), - salt: generatePseudoRandomSalt(), - expirationTimeSeconds: getRandomInteger(0, 2 ** 64), - makerAssetData: MAKER_ASSET_DATA, - takerAssetData: TAKER_ASSET_DATA, - makerFeeAssetData: constants.NULL_BYTES, - takerFeeAssetData: constants.NULL_BYTES, - makerAssetAmount: getRandomInteger(1, 1e18), - takerAssetAmount: getRandomInteger(1, 1e18), - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - signature: hexUtils.random(), - ...overrides, - }; + function createOrdersFromSellRates(takerAmount: BigNumber, rates: Numberish[]): SignedNativeOrder[] { + const singleTakerAmount = takerAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); + return rates.map(r => { + const o: SignedNativeOrder = { + order: { + ...new LimitOrder({ + makerAmount: singleTakerAmount.times(r).integerValue(), + takerAmount: singleTakerAmount, + }), + }, + signature: SIGNATURE, + type: FillQuoteTransformerOrderType.Limit, + }; + return o; + }); } - function getSourceFromAssetData(assetData: string): ERC20BridgeSource { - if (assetData.length === 74) { - return ERC20BridgeSource.Native; - } - const bridgeData = assetDataUtils.decodeAssetDataOrThrow(assetData); - if (!assetDataUtils.isERC20BridgeAssetData(bridgeData)) { - throw new Error('AssetData is not ERC20BridgeAssetData'); - } - const { bridgeAddress } = bridgeData; - switch (bridgeAddress) { - case contractAddresses.kyberBridge.toLowerCase(): - return ERC20BridgeSource.Kyber; - case contractAddresses.eth2DaiBridge.toLowerCase(): - return ERC20BridgeSource.Eth2Dai; - case contractAddresses.uniswapBridge.toLowerCase(): - return ERC20BridgeSource.Uniswap; - case contractAddresses.uniswapV2Bridge.toLowerCase(): - return ERC20BridgeSource.UniswapV2; - case contractAddresses.curveBridge.toLowerCase(): - return ERC20BridgeSource.Curve; - case contractAddresses.mStableBridge.toLowerCase(): - return ERC20BridgeSource.MStable; - case contractAddresses.mooniswapBridge.toLowerCase(): - return ERC20BridgeSource.Mooniswap; - case contractAddresses.sushiswapBridge.toLowerCase(): - return ERC20BridgeSource.SushiSwap; - case contractAddresses.shellBridge.toLowerCase(): - return ERC20BridgeSource.Shell; - case contractAddresses.dodoBridge.toLowerCase(): - return ERC20BridgeSource.Dodo; - default: - break; - } - throw new Error(`Unknown bridge address: ${bridgeAddress}`); - } - - function assertSamePrefix(actual: string, expected: string): void { - expect(actual.substr(0, expected.length)).to.eq(expected); - } - - function createOrdersFromSellRates(takerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] { - const singleTakerAssetAmount = takerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); - return rates.map(r => - createOrder({ - makerAssetAmount: singleTakerAssetAmount.times(r).integerValue(), - takerAssetAmount: singleTakerAssetAmount, - }), - ); - } - - function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] { - const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); - return rates.map(r => - createOrder({ - makerAssetAmount: singleMakerAssetAmount, - takerAssetAmount: singleMakerAssetAmount.div(r).integerValue(), - }), - ); + function createOrdersFromBuyRates(makerAmount: BigNumber, rates: Numberish[]): SignedNativeOrder[] { + const singleMakerAmount = makerAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); + return rates.map(r => { + const o: SignedNativeOrder = { + order: { + ...new LimitOrder({ + makerAmount: singleMakerAmount, + takerAmount: singleMakerAmount.div(r).integerValue(), + }), + }, + signature: SIGNATURE, + type: FillQuoteTransformerOrderType.Limit, + }; + return o; + }); } const ORDER_DOMAIN = { @@ -368,7 +350,7 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() }, [ERC20BridgeSource.SushiSwap]: { tokenAddressPath: [] }, [ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() }, - [ERC20BridgeSource.Native]: { order: createOrder() }, + [ERC20BridgeSource.Native]: { order: new LimitOrder() }, [ERC20BridgeSource.MultiHop]: {}, [ERC20BridgeSource.Shell]: { poolAddress: randomAddress() }, [ERC20BridgeSource.Cream]: { poolAddress: randomAddress() }, @@ -381,11 +363,11 @@ describe('MarketOperationUtils tests', () => { const result = new BigNumber(18); return [result, result]; }, - getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { - return orders.map(o => o.takerAssetAmount); + getLimitOrderFillableTakerAmounts(orders: SignedNativeOrder[]): BigNumber[] { + return orders.map(o => o.order.takerAmount); }, - getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { - return orders.map(o => o.makerAssetAmount); + getLimitOrderFillableMakerAmounts(orders: SignedNativeOrder[]): BigNumber[] { + return orders.map(o => o.order.makerAmount); }, getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), @@ -439,6 +421,7 @@ describe('MarketOperationUtils tests', () => { ), getTwoHopSellQuotes: (..._params: any[]) => [], getTwoHopBuyQuotes: (..._params: any[]) => [], + isAddressContract: (..._params: any[]) => false, }; const MOCK_SAMPLER = ({ @@ -458,43 +441,6 @@ describe('MarketOperationUtils tests', () => { Object.assign(MOCK_SAMPLER, ops); } - describe('getRfqtIndicativeQuotesAsync', () => { - const partialRfqt: RfqtRequestOpts = { - apiKey: 'foo', - takerAddress: NULL_ADDRESS, - isIndicative: true, - intentOnFilling: false, - }; - - it('calls RFQT', async () => { - const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose); - requestor - .setup(r => - r.requestRfqtIndicativeQuotesAsync( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve([])) - .verifiable(TypeMoq.Times.once()); - await getRfqtIndicativeQuotesAsync( - MAKER_ASSET_DATA, - TAKER_ASSET_DATA, - MarketOperation.Sell, - new BigNumber('100e18'), - undefined, - { - rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, - }, - ); - requestor.verifyAll(); - }); - }); - describe('MarketOperationUtils', () => { let marketOperationUtils: MarketOperationUtils; @@ -539,7 +485,7 @@ describe('MarketOperationUtils tests', () => { ); }, }); - await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples, }); @@ -581,7 +527,7 @@ describe('MarketOperationUtils tests', () => { return DEFAULT_OPS.getCreamSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); }, }); - await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); @@ -627,7 +573,7 @@ describe('MarketOperationUtils tests', () => { return DEFAULT_OPS.getCreamSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); }, }); - await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); @@ -673,7 +619,7 @@ describe('MarketOperationUtils tests', () => { return DEFAULT_OPS.getCreamSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); }, }); - await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], includedSources, @@ -681,30 +627,32 @@ describe('MarketOperationUtils tests', () => { expect(_.uniq(sourcesPolled).sort()).to.deep.equals(includedSources.sort()); }); - it('generates bridge orders with correct asset data', async () => { - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( - // Pass in empty orders to prevent native orders from being used. - ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), - FILL_AMOUNT, - DEFAULT_OPTS, - ); - const improvedOrders = improvedOrdersResponse.optimizedOrders; - expect(improvedOrders).to.not.be.length(0); - for (const order of improvedOrders) { - expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); - const makerAssetDataPrefix = hexUtils.slice( - assetDataUtils.encodeERC20BridgeAssetData( - MAKER_TOKEN, - constants.NULL_ADDRESS, - constants.NULL_BYTES, - ), - 0, - 36, - ); - assertSamePrefix(order.makerAssetData, makerAssetDataPrefix); - expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA); - } - }); + // // TODO (xianny): v4 will have a new way of representing bridge data + // it('generates bridge orders with correct asset data', async () => { + // const improvedOrdersResponse = await getMarketSellOrdersAsync( + // marketOperationUtils, + // // Pass in empty orders to prevent native orders from being used. + // ORDERS.map(o => ({ ...o, makerAmount: constants.ZERO_AMOUNT })), + // FILL_AMOUNT, + // DEFAULT_OPTS, + // ); + // const improvedOrders = improvedOrdersResponse.optimizedOrders; + // expect(improvedOrders).to.not.be.length(0); + // for (const order of improvedOrders) { + // expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); + // const makerAssetDataPrefix = hexUtils.slice( + // assetDataUtils.encodeERC20BridgeAssetData( + // MAKER_TOKEN, + // constants.NULL_ADDRESS, + // constants.NULL_BYTES, + // ), + // 0, + // 36, + // ); + // assertSamePrefix(order.makerAssetData, makerAssetDataPrefix); + // expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA); + // } + // }); it('getMarketSellOrdersAsync() optimizer will be called once only if price-aware RFQ is disabled', async () => { const mockedMarketOpUtils = TypeMoq.Mock.ofType( @@ -723,8 +671,13 @@ describe('MarketOperationUtils tests', () => { .returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b)) .verifiable(TypeMoq.Times.once()); - const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b)); - await mockedMarketOpUtils.object.getMarketSellOrdersAsync(ORDERS, totalAssetAmount, DEFAULT_OPTS); + const totalAssetAmount = ORDERS.map(o => o.order.takerAmount).reduce((a, b) => a.plus(b)); + await mockedMarketOpUtils.object.getOptimizerResultAsync( + ORDERS, + totalAssetAmount, + MarketOperation.Sell, + DEFAULT_OPTS, + ); mockedMarketOpUtils.verifyAll(); }); @@ -752,8 +705,8 @@ describe('MarketOperationUtils tests', () => { ) .callback( ( - _makerAssetData: string, - _takerAssetData: string, + _makerToken: string, + _takerToken: string, _assetFillAmount: BigNumber, _marketOperation: MarketOperation, comparisonPrice: BigNumber | undefined, @@ -765,12 +718,16 @@ describe('MarketOperationUtils tests', () => { .returns(async () => { return [ { - signedOrder: createOrder({ - makerAssetData: MAKER_ASSET_DATA, - takerAssetData: TAKER_ASSET_DATA, - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(321, 6), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18), - }), + order: { + ...new RfqOrder({ + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + makerAmount: Web3Wrapper.toBaseUnitAmount(321, 6), + takerAmount: Web3Wrapper.toBaseUnitAmount(1, 18), + }), + }, + signature: SIGNATURE, + type: FillQuoteTransformerOrderType.Rfq, }, ]; }); @@ -791,32 +748,42 @@ describe('MarketOperationUtils tests', () => { ) .returns(async () => { return { - dexQuotes: [], - ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18), - ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6), + side: MarketOperation.Sell, inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18), inputToken: MAKER_TOKEN, outputToken: TAKER_TOKEN, - nativeOrders: [ - createOrder({ - makerAssetData: MAKER_ASSET_DATA, - takerAssetData: TAKER_ASSET_DATA, - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(320, 6), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18), - }), - ], - orderFillableAmounts: [Web3Wrapper.toBaseUnitAmount(1, 18)], - rfqtIndicativeQuotes: [], - side: MarketOperation.Sell, - twoHopQuotes: [], + ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18), + ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6), quoteSourceFilters: new SourceFilters(), makerTokenDecimals: 6, takerTokenDecimals: 18, + quotes: { + dexQuotes: [], + rfqtIndicativeQuotes: [], + twoHopQuotes: [], + nativeOrders: [ + { + order: new LimitOrder({ + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + makerAmount: Web3Wrapper.toBaseUnitAmount(320, 6), + takerAmount: Web3Wrapper.toBaseUnitAmount(1, 18), + }), + fillableTakerAmount: Web3Wrapper.toBaseUnitAmount(1, 18), + fillableMakerAmount: Web3Wrapper.toBaseUnitAmount(320, 6), + fillableTakerFeeAmount: new BigNumber(0), + type: FillQuoteTransformerOrderType.Limit, + signature: SIGNATURE, + }, + ], + }, + isRfqSupported: true, }; }); - const result = await mockedMarketOpUtils.object.getMarketSellOrdersAsync( + const result = await mockedMarketOpUtils.object.getOptimizerResultAsync( ORDERS, Web3Wrapper.toBaseUnitAmount(1, 18), + MarketOperation.Sell, { ...DEFAULT_OPTS, feeSchedule, @@ -824,8 +791,8 @@ describe('MarketOperationUtils tests', () => { isIndicative: false, apiKey: 'foo', takerAddress: randomAddress(), + txOrigin: randomAddress(), intentOnFilling: true, - priceAwareRFQFlag: PRICE_AWARE_RFQ_ENABLED, quoteRequestor: { requestRfqtFirmQuotesAsync: mockedQuoteRequestor.object.requestRfqtFirmQuotesAsync, } as any, @@ -835,8 +802,8 @@ describe('MarketOperationUtils tests', () => { expect(result.optimizedOrders.length).to.eql(1); // tslint:disable-next-line:no-unnecessary-type-assertion expect(requestedComparisonPrice!.toString()).to.eql('320'); - expect(result.optimizedOrders[0].makerAssetAmount.toString()).to.eql('321000000'); - expect(result.optimizedOrders[0].takerAssetAmount.toString()).to.eql('1000000000000000000'); + expect(result.optimizedOrders[0].makerAmount.toString()).to.eql('321000000'); + expect(result.optimizedOrders[0].takerAmount.toString()).to.eql('1000000000000000000'); }); it('getMarketSellOrdersAsync() will not rerun the optimizer if no orders are returned', async () => { @@ -857,20 +824,25 @@ describe('MarketOperationUtils tests', () => { const requestor = getMockedQuoteRequestor('firm', [], TypeMoq.Times.once()); - const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b)); - await mockedMarketOpUtils.object.getMarketSellOrdersAsync(ORDERS, totalAssetAmount, { - ...DEFAULT_OPTS, - rfqt: { - isIndicative: false, - apiKey: 'foo', - takerAddress: randomAddress(), - intentOnFilling: true, - priceAwareRFQFlag: PRICE_AWARE_RFQ_ENABLED, - quoteRequestor: { - requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync, - } as any, + const totalAssetAmount = ORDERS.map(o => o.order.takerAmount).reduce((a, b) => a.plus(b)); + await mockedMarketOpUtils.object.getOptimizerResultAsync( + ORDERS, + totalAssetAmount, + MarketOperation.Sell, + { + ...DEFAULT_OPTS, + rfqt: { + isIndicative: false, + apiKey: 'foo', + takerAddress: randomAddress(), + intentOnFilling: true, + txOrigin: randomAddress(), + quoteRequestor: { + requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync, + } as any, + }, }, - }); + ); mockedMarketOpUtils.verifyAll(); requestor.verifyAll(); }); @@ -893,23 +865,24 @@ describe('MarketOperationUtils tests', () => { mockedMarketOpUtils .setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => { - numOrdersInCall.push(msl.nativeOrders.length); - numIndicativeQuotesInCall.push(msl.rfqtIndicativeQuotes.length); + numOrdersInCall.push(msl.quotes.nativeOrders.length); + numIndicativeQuotesInCall.push(msl.quotes.rfqtIndicativeQuotes.length); }) .returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b)) .verifiable(TypeMoq.Times.exactly(2)); - const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b)); - await mockedMarketOpUtils.object.getMarketSellOrdersAsync( + const totalAssetAmount = ORDERS.map(o => o.order.takerAmount).reduce((a, b) => a.plus(b)); + await mockedMarketOpUtils.object.getOptimizerResultAsync( ORDERS.slice(2, ORDERS.length), totalAssetAmount, + MarketOperation.Sell, { ...DEFAULT_OPTS, rfqt: { isIndicative: true, apiKey: 'foo', - priceAwareRFQFlag: PRICE_AWARE_RFQ_ENABLED, takerAddress: randomAddress(), + txOrigin: randomAddress(), intentOnFilling: true, quoteRequestor: { requestRfqtIndicativeQuotesAsync: requestor.object.requestRfqtIndicativeQuotesAsync, @@ -951,15 +924,16 @@ describe('MarketOperationUtils tests', () => { mockedMarketOpUtils .setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .callback(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => { - numOrdersInCall.push(msl.nativeOrders.length); + numOrdersInCall.push(msl.quotes.nativeOrders.length); }) .returns(async (a, b) => mockedMarketOpUtils.target._generateOptimizedOrdersAsync(a, b)) .verifiable(TypeMoq.Times.exactly(2)); - const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b)); - await mockedMarketOpUtils.object.getMarketSellOrdersAsync( + const totalAssetAmount = ORDERS.map(o => o.order.takerAmount).reduce((a, b) => a.plus(b)); + await mockedMarketOpUtils.object.getOptimizerResultAsync( ORDERS.slice(1, ORDERS.length), totalAssetAmount, + MarketOperation.Sell, { ...DEFAULT_OPTS, rfqt: { @@ -967,7 +941,7 @@ describe('MarketOperationUtils tests', () => { apiKey: 'foo', takerAddress: randomAddress(), intentOnFilling: true, - priceAwareRFQFlag: PRICE_AWARE_RFQ_ENABLED, + txOrigin: randomAddress(), quoteRequestor: { requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync, } as any, @@ -1001,10 +975,10 @@ describe('MarketOperationUtils tests', () => { mockedMarketOpUtils .setup(m => m._generateOptimizedOrdersAsync(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(async (msl: MarketSideLiquidity, _opts: GenerateOptimizedOrdersOpts) => { - if (msl.nativeOrders.length === 1) { + if (msl.quotes.nativeOrders.length === 1) { hasFirstOptimizationRun = true; throw new Error(AggregationError.NoOptimalPath); - } else if (msl.nativeOrders.length === 3) { + } else if (msl.quotes.nativeOrders.length === 3) { hasSecondOptimizationRun = true; return mockedMarketOpUtils.target._generateOptimizedOrdersAsync(msl, _opts); } else { @@ -1013,17 +987,18 @@ describe('MarketOperationUtils tests', () => { }) .verifiable(TypeMoq.Times.exactly(2)); - const totalAssetAmount = ORDERS.map(o => o.takerAssetAmount).reduce((a, b) => a.plus(b)); - await mockedMarketOpUtils.object.getMarketSellOrdersAsync( + const totalAssetAmount = ORDERS.map(o => o.order.takerAmount).reduce((a, b) => a.plus(b)); + await mockedMarketOpUtils.object.getOptimizerResultAsync( ORDERS.slice(2, ORDERS.length), totalAssetAmount, + MarketOperation.Sell, { ...DEFAULT_OPTS, rfqt: { isIndicative: false, apiKey: 'foo', takerAddress: randomAddress(), - priceAwareRFQFlag: PRICE_AWARE_RFQ_ENABLED, + txOrigin: randomAddress(), intentOnFilling: true, quoteRequestor: { requestRfqtFirmQuotesAsync: requestor.object.requestRfqtFirmQuotesAsync, @@ -1056,9 +1031,10 @@ describe('MarketOperationUtils tests', () => { .verifiable(TypeMoq.Times.exactly(1)); try { - await mockedMarketOpUtils.object.getMarketSellOrdersAsync( + await mockedMarketOpUtils.object.getOptimizerResultAsync( ORDERS.slice(2, ORDERS.length), - ORDERS[0].takerAssetAmount, + ORDERS[0].order.takerAmount, + MarketOperation.Sell, DEFAULT_OPTS, ); expect.fail(`Call should have thrown "${AggregationError.NoOptimalPath}" but instead succeded`); @@ -1071,22 +1047,24 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct taker amount', async () => { - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, // Pass in empty orders to prevent native orders from being used. - ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), + ORDERS.map(o => ({ ...o, makerAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount)); - expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); + const totaltakerAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAmount)); + expect(totaltakerAmount).to.bignumber.gte(FILL_AMOUNT); }); it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, // Pass in empty orders to prevent native orders from being used. - ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), + ORDERS.map(o => ({ ...o, makerAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, { ...DEFAULT_OPTS, bridgeSlippage }, ); @@ -1094,7 +1072,7 @@ describe('MarketOperationUtils tests', () => { expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const expectedMakerAmount = order.fills[0].output; - const slippage = new BigNumber(1).minus(order.makerAssetAmount.div(expectedMakerAmount.plus(1))); + const slippage = new BigNumber(1).minus(order.makerAmount.div(expectedMakerAmount.plus(1))); assertRoughlyEquals(slippage, bridgeSlippage, 1); } }); @@ -1108,7 +1086,8 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, @@ -1147,7 +1126,8 @@ describe('MarketOperationUtils tests', () => { getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, @@ -1185,7 +1165,8 @@ describe('MarketOperationUtils tests', () => { getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, @@ -1211,7 +1192,8 @@ describe('MarketOperationUtils tests', () => { getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, @@ -1235,7 +1217,8 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, @@ -1256,7 +1239,8 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await getMarketSellOrdersAsync( + marketOperationUtils, createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, @@ -1279,19 +1263,24 @@ describe('MarketOperationUtils tests', () => { gasCost: 0, }; replaceSamplerOps({ - getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], + getLimitOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); const sampler = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN); - const ordersAndReport = await sampler.getMarketSellOrdersAsync( + const ordersAndReport = await sampler.getOptimizerResultAsync( [ - createOrder({ - makerAssetData: assetDataUtils.encodeERC20AssetData(MAKER_TOKEN), - takerAssetData: assetDataUtils.encodeERC20AssetData(TAKER_TOKEN), - }), + { + order: new LimitOrder({ + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + }), + type: FillQuoteTransformerOrderType.Limit, + signature: {} as any, + }, ], FILL_AMOUNT, + MarketOperation.Sell, { includedSources: [ERC20BridgeSource.LiquidityProvider], excludedSources: [], @@ -1301,15 +1290,18 @@ describe('MarketOperationUtils tests', () => { ); const result = ordersAndReport.optimizedOrders; expect(result.length).to.eql(1); - expect(result[0].makerAddress).to.eql(liquidityProviderAddress); + expect( + (result[0] as OptimizedMarketBridgeOrder).fillData.poolAddress, + ).to.eql(liquidityProviderAddress); - // tslint:disable-next-line:no-unnecessary-type-assertion - const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow( - result[0].makerAssetData, - ) as ERC20BridgeAssetData; - expect(decodedAssetData.assetProxyId).to.eql(AssetProxyId.ERC20Bridge); - expect(decodedAssetData.bridgeAddress).to.eql(liquidityProviderAddress); - expect(result[0].takerAssetAmount).to.bignumber.eql(FILL_AMOUNT); + // // TODO (xianny): decode bridge data in v4 format + // // tslint:disable-next-line:no-unnecessary-type-assertion + // const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow( + // result[0].makerAssetData, + // ) as ERC20BridgeAssetData; + // expect(decodedAssetData.assetProxyId).to.eql(AssetProxyId.ERC20Bridge); + // expect(decodedAssetData.bridgeAddress).to.eql(liquidityProviderAddress); + // expect(result[0].takerAmount).to.bignumber.eql(FILL_AMOUNT); }); it('factors in exchange proxy gas overhead', async () => { @@ -1334,9 +1326,10 @@ describe('MarketOperationUtils tests', () => { sourceFlags === SOURCE_FLAGS.LiquidityProvider ? constants.ZERO_AMOUNT : new BigNumber(1.3e5).times(gasPrice); - const improvedOrdersResponse = await optimizer.getMarketSellOrdersAsync( + const improvedOrdersResponse = await optimizer.getOptimizerResultAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, + MarketOperation.Sell, { ...DEFAULT_OPTS, numSamples: 4, @@ -1393,7 +1386,7 @@ describe('MarketOperationUtils tests', () => { ); }, }); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples, }); @@ -1438,7 +1431,7 @@ describe('MarketOperationUtils tests', () => { return DEFAULT_OPS.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); }, }); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); @@ -1484,7 +1477,7 @@ describe('MarketOperationUtils tests', () => { return DEFAULT_OPS.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); }, }); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); @@ -1530,7 +1523,7 @@ describe('MarketOperationUtils tests', () => { return DEFAULT_OPS.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); }, }); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { + await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], includedSources, @@ -1538,48 +1531,51 @@ describe('MarketOperationUtils tests', () => { expect(_.uniq(sourcesPolled).sort()).to.deep.eq(includedSources.sort()); }); - it('generates bridge orders with correct asset data', async () => { - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( - // Pass in empty orders to prevent native orders from being used. - ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), - FILL_AMOUNT, - DEFAULT_OPTS, - ); - const improvedOrders = improvedOrdersResponse.optimizedOrders; - expect(improvedOrders).to.not.be.length(0); - for (const order of improvedOrders) { - expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); - const makerAssetDataPrefix = hexUtils.slice( - assetDataUtils.encodeERC20BridgeAssetData( - MAKER_TOKEN, - constants.NULL_ADDRESS, - constants.NULL_BYTES, - ), - 0, - 36, - ); - assertSamePrefix(order.makerAssetData, makerAssetDataPrefix); - expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA); - } - }); + // it('generates bridge orders with correct asset data', async () => { + // const improvedOrdersResponse = await getMarketBuyOrdersAsync( + // marketOperationUtils, + // // Pass in empty orders to prevent native orders from being used. + // ORDERS.map(o => ({ ...o, makerAmount: constants.ZERO_AMOUNT })), + // FILL_AMOUNT, + // DEFAULT_OPTS, + // ); + // const improvedOrders = improvedOrdersResponse.optimizedOrders; + // expect(improvedOrders).to.not.be.length(0); + // for (const order of improvedOrders) { + // expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); + // const makerAssetDataPrefix = hexUtils.slice( + // assetDataUtils.encodeERC20BridgeAssetData( + // MAKER_TOKEN, + // constants.NULL_ADDRESS, + // constants.NULL_BYTES, + // ), + // 0, + // 36, + // ); + // assertSamePrefix(order.makerAssetData, makerAssetDataPrefix); + // expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA); + // } + // }); it('generates bridge orders with correct maker amount', async () => { - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, // Pass in empty orders to prevent native orders from being used. - ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), + ORDERS.map(o => ({ ...o, makerAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount)); - expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); + const totalmakerAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAmount)); + expect(totalmakerAmount).to.bignumber.gte(FILL_AMOUNT); }); it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, // Pass in empty orders to prevent native orders from being used. - ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), + ORDERS.map(o => ({ ...o, makerAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, { ...DEFAULT_OPTS, bridgeSlippage }, ); @@ -1587,7 +1583,7 @@ describe('MarketOperationUtils tests', () => { expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const expectedTakerAmount = order.fills[0].output; - const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).minus(1); + const slippage = order.takerAmount.div(expectedTakerAmount.plus(1)).minus(1); assertRoughlyEquals(slippage, bridgeSlippage, 1); } }); @@ -1600,7 +1596,8 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, @@ -1640,7 +1637,8 @@ describe('MarketOperationUtils tests', () => { getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, @@ -1678,7 +1676,8 @@ describe('MarketOperationUtils tests', () => { getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, @@ -1701,7 +1700,8 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, @@ -1721,7 +1721,8 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await getMarketBuyOrdersAsync( + marketOperationUtils, createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, @@ -1756,9 +1757,10 @@ describe('MarketOperationUtils tests', () => { sourceFlags === SOURCE_FLAGS.LiquidityProvider ? constants.ZERO_AMOUNT : new BigNumber(1.3e5).times(gasPrice); - const improvedOrdersResponse = await optimizer.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await optimizer.getOptimizerResultAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, + MarketOperation.Buy, { ...DEFAULT_OPTS, numSamples: 4, @@ -1780,32 +1782,38 @@ describe('MarketOperationUtils tests', () => { }); describe('createFills', () => { - const takerAssetAmount = new BigNumber(5000000); + const takerAmount = new BigNumber(5000000); const ethToOutputRate = new BigNumber(0.5); // tslint:disable-next-line:no-object-literal-type-assertion - const smallOrder = { - chainId: 1, - makerAddress: 'SMALL_ORDER', - takerAddress: NULL_ADDRESS, - takerAssetAmount, - makerAssetAmount: takerAssetAmount.times(2), - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, - makerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - takerAssetData: '0xf47261b0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - makerFeeAssetData: '0x', - takerFeeAssetData: '0x', - fillableTakerAssetAmount: takerAssetAmount, - fillableMakerAssetAmount: takerAssetAmount.times(2), - fillableTakerFeeAmount: ZERO_AMOUNT, - } as SignedOrderWithFillableAmounts; - const largeOrder = { - ...smallOrder, - makerAddress: 'LARGE_ORDER', - fillableMakerAssetAmount: smallOrder.fillableMakerAssetAmount.times(2), - fillableTakerAssetAmount: smallOrder.fillableTakerAssetAmount.times(2), - makerAssetAmount: smallOrder.makerAssetAmount.times(2), - takerAssetAmount: smallOrder.takerAssetAmount.times(2), + const smallOrder: NativeOrderWithFillableAmounts = { + order: { + ...new LimitOrder({ + chainId: 1, + maker: 'SMALL_ORDER', + takerAmount, + makerAmount: takerAmount.times(2), + }), + }, + fillableMakerAmount: takerAmount.times(2), + fillableTakerAmount: takerAmount, + fillableTakerFeeAmount: new BigNumber(0), + type: FillQuoteTransformerOrderType.Limit, + signature: SIGNATURE, + }; + const largeOrder: NativeOrderWithFillableAmounts = { + order: { + ...new LimitOrder({ + chainId: 1, + maker: 'LARGE_ORDER', + takerAmount: smallOrder.order.takerAmount.times(2), + makerAmount: smallOrder.order.makerAmount.times(2), + }), + }, + fillableTakerAmount: smallOrder.fillableTakerAmount.times(2), + fillableMakerAmount: smallOrder.fillableMakerAmount.times(2), + fillableTakerFeeAmount: new BigNumber(0), + type: FillQuoteTransformerOrderType.Limit, + signature: SIGNATURE, }; const orders = [smallOrder, largeOrder]; const feeSchedule = { @@ -1817,12 +1825,12 @@ describe('MarketOperationUtils tests', () => { side: MarketOperation.Sell, orders, dexQuotes: [], - targetInput: takerAssetAmount.minus(1), + targetInput: takerAmount.minus(1), ethToOutputRate, feeSchedule, }); - expect((path[0][0].fillData as NativeFillData).order.makerAddress).to.eq(smallOrder.makerAddress); - expect(path[0][0].input).to.be.bignumber.eq(takerAssetAmount.minus(1)); + expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker); + expect(path[0][0].input).to.be.bignumber.eq(takerAmount.minus(1)); }); it('penalizes native fill based on available amount when target is larger', () => { @@ -1834,8 +1842,8 @@ describe('MarketOperationUtils tests', () => { ethToOutputRate, feeSchedule, }); - expect((path[0][0].fillData as NativeFillData).order.makerAddress).to.eq(largeOrder.makerAddress); - expect((path[0][1].fillData as NativeFillData).order.makerAddress).to.eq(smallOrder.makerAddress); + expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker); + expect((path[0][1].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker); }); }); }); diff --git a/packages/asset-swapper/test/order_prune_utils_test.ts b/packages/asset-swapper/test/order_prune_utils_test.ts deleted file mode 100644 index 65829c5fc3..0000000000 --- a/packages/asset-swapper/test/order_prune_utils_test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers'; -import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; -import { migrateOnceAsync } from '@0x/migrations'; -import { assetDataUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { constants } from '../src/constants'; -import { OrderPrunerPermittedFeeTypes } from '../src/types'; -import { orderPrunerUtils } from '../src/utils/order_prune_utils'; - -import { chaiSetup } from './utils/chai_setup'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); -const PROTOCOL_FEE_MULTIPLIER = 70000; -const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER); -const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers -const EXPIRY_BUFFER_MS = 120000; - -// tslint:disable: no-unused-expression -// tslint:disable: custom-no-magic-numbers -describe('orderPrunerUtils', () => { - let erc20MakerTokenContract: ERC20TokenContract; - let erc20TakerTokenContract: ERC20TokenContract; - let exchangeContract: ExchangeContract; - let userAddresses: string[]; - let coinbaseAddress: string; - let makerAddress: string; - let takerAddress: string; - let feeRecipient: string; - let makerTokenAddress: string; - let takerTokenAddress: string; - let makerAssetData: string; - let takerAssetData: string; - let orderFactory: OrderFactory; - let contractAddresses: ContractAddresses; - - let nonOpenSignedOrder: SignedOrder; - let expiredOpenSignedOrder: SignedOrder; - let partiallyFilledOpenSignedOrderFeeless: SignedOrder; - let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder; - let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder; - - const chainId = TESTRPC_CHAIN_ID; - const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI); - const partialFillAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI); - const takerFeeAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI); - - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - await blockchainLifecycle.startAsync(); - userAddresses = await web3Wrapper.getAvailableAddressesAsync(); - [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; - [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); - erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider); - erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider); - exchangeContract = new ExchangeContract(contractAddresses.exchange, provider); - - [makerAssetData, takerAssetData] = [ - assetDataUtils.encodeERC20AssetData(makerTokenAddress), - assetDataUtils.encodeERC20AssetData(takerTokenAddress), - ]; - - // Configure order defaults - const defaultOrderParams = { - ...devConstants.STATIC_ORDER_PARAMS, - makerAddress, - takerAddress: constants.NULL_ADDRESS, - makerAssetData, - takerAssetData, - makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - feeRecipientAddress: feeRecipient, - exchangeAddress: contractAddresses.exchange, - chainId, - }; - const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - - nonOpenSignedOrder = await orderFactory.newSignedOrderAsync({ - takerAddress, - }); - - expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({ - expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).plus(60000), - }); - - // give double fillableAmount to maker and taker as buffer - await erc20MakerTokenContract - .transfer(makerAddress, fillableAmount.multipliedBy(4)) - .sendTransactionAsync({ from: coinbaseAddress }); - await erc20TakerTokenContract - .transfer(takerAddress, fillableAmount.multipliedBy(4)) - .sendTransactionAsync({ from: coinbaseAddress }); - await erc20MakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS) - .sendTransactionAsync({ from: makerAddress }); - await erc20MakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS) - .sendTransactionAsync({ from: takerAddress }); - await erc20TakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS) - .sendTransactionAsync({ from: takerAddress }); - - partiallyFilledOpenSignedOrderFeeless = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - }); - - await exchangeContract - .fillOrKillOrder( - partiallyFilledOpenSignedOrderFeeless, - partialFillAmount, - partiallyFilledOpenSignedOrderFeeless.signature, - ) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - - partiallyFilledOpenSignedOrderFeeInTakerAsset = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - takerFee: takerFeeAmount, - takerFeeAssetData: takerAssetData, - }); - - await exchangeContract - .fillOrKillOrder( - partiallyFilledOpenSignedOrderFeeInTakerAsset, - partialFillAmount, - partiallyFilledOpenSignedOrderFeeInTakerAsset.signature, - ) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - - partiallyFilledOpenSignedOrderFeeInMakerAsset = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - takerFee: takerFeeAmount, - takerFeeAssetData: makerAssetData, - }); - - await exchangeContract - .fillOrKillOrder( - partiallyFilledOpenSignedOrderFeeInMakerAsset, - partialFillAmount, - partiallyFilledOpenSignedOrderFeeInMakerAsset.signature, - ) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('prunedForUsableSignedOrders', () => { - it('should filter for only feeless orders if options permit only feeless orders', async () => { - const permittedOrderFeeTypes = new Set([OrderPrunerPermittedFeeTypes.NoFees]); - const orders = [ - partiallyFilledOpenSignedOrderFeeInMakerAsset, - partiallyFilledOpenSignedOrderFeeInTakerAsset, - partiallyFilledOpenSignedOrderFeeless, - ]; - const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - permittedOrderFeeTypes, - EXPIRY_BUFFER_MS, - ); - // checks for one order in results and check for signature of orders - expect(resultPrunedOrders.length).to.be.equal(1); - expect(resultPrunedOrders[0].signature).to.be.deep.equal(partiallyFilledOpenSignedOrderFeeless.signature); - }); - it('should filter for only takerFee in takerAsset orders if options permit only takerFee in takerAsset orders', async () => { - const permittedOrderFeeTypes = new Set([ - OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee, - ]); - const orders = [ - partiallyFilledOpenSignedOrderFeeInMakerAsset, - partiallyFilledOpenSignedOrderFeeInTakerAsset, - partiallyFilledOpenSignedOrderFeeless, - ]; - const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - permittedOrderFeeTypes, - EXPIRY_BUFFER_MS, - ); - // checks for one order in results and check for signature of orders - expect(resultPrunedOrders.length).to.be.equal(1); - expect(resultPrunedOrders[0].signature).to.be.deep.equal( - partiallyFilledOpenSignedOrderFeeInTakerAsset.signature, - ); - }); - it('should filter for only makerFee in takerAsset orders if options permit only makerFee orders', async () => { - const permittedOrderFeeTypes = new Set([ - OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee, - ]); - const orders = [ - partiallyFilledOpenSignedOrderFeeInMakerAsset, - partiallyFilledOpenSignedOrderFeeInTakerAsset, - partiallyFilledOpenSignedOrderFeeless, - ]; - const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - permittedOrderFeeTypes, - EXPIRY_BUFFER_MS, - ); - // checks for one order in results and check for signature of orders - expect(resultPrunedOrders.length).to.be.equal(1); - expect(resultPrunedOrders[0].signature).to.be.deep.equal( - partiallyFilledOpenSignedOrderFeeInMakerAsset.signature, - ); - }); - it('should filter out non open orders', async () => { - const permittedOrderFeeTypes = new Set([ - OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee, - OrderPrunerPermittedFeeTypes.NoFees, - OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee, - ]); - const orders = [nonOpenSignedOrder]; - const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - permittedOrderFeeTypes, - EXPIRY_BUFFER_MS, - ); - expect(resultPrunedOrders).to.be.empty; - }); - it('should filter out expired orders', async () => { - const permittedOrderFeeTypes = new Set([ - OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee, - OrderPrunerPermittedFeeTypes.NoFees, - OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee, - ]); - const orders = [expiredOpenSignedOrder]; - const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders( - orders, - permittedOrderFeeTypes, - EXPIRY_BUFFER_MS, - ); - expect(resultPrunedOrders).to.be.empty; - }); - }); -}); diff --git a/packages/asset-swapper/test/order_state_utils_test.ts b/packages/asset-swapper/test/order_state_utils_test.ts deleted file mode 100644 index c68eb4041b..0000000000 --- a/packages/asset-swapper/test/order_state_utils_test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { DevUtilsContract, ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers'; -import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; -import { migrateOnceAsync } from '@0x/migrations'; -import { assetDataUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { constants } from '../src/constants'; -import { SignedOrderWithFillableAmounts } from '../src/types'; -import { OrderStateUtils } from '../src/utils/order_state_utils'; - -import { chaiSetup } from './utils/chai_setup'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); -const PROTOCOL_FEE_MULTIPLIER = 70000; -const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER); -const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers - -const isSignedOrdersWithFillableAmountsNotFillable = (signedOrders: SignedOrderWithFillableAmounts[]) => { - signedOrders.forEach(order => { - expect(order.fillableMakerAssetAmount).to.bignumber.eq(constants.ZERO_AMOUNT); - expect(order.fillableTakerAssetAmount).to.bignumber.eq(constants.ZERO_AMOUNT); - expect(order.fillableTakerFeeAmount).to.bignumber.eq(constants.ZERO_AMOUNT); - }); -}; - -// tslint:disable: no-unused-expression -// tslint:disable: custom-no-magic-numbers -describe('OrderStateUtils', () => { - let erc20MakerTokenContract: ERC20TokenContract; - let erc20TakerTokenContract: ERC20TokenContract; - let exchangeContract: ExchangeContract; - let userAddresses: string[]; - let coinbaseAddress: string; - let makerAddress: string; - let takerAddress: string; - let feeRecipient: string; - let makerTokenAddress: string; - let takerTokenAddress: string; - let makerAssetData: string; - let takerAssetData: string; - let orderFactory: OrderFactory; - let contractAddresses: ContractAddresses; - let orderStateUtils: OrderStateUtils; - - let expiredOpenSignedOrder: SignedOrder; - let invalidSignatureOpenSignedOrder: SignedOrder; - let fullyFillableOpenSignedOrder: SignedOrder; - let partiallyFilledOpenSignedOrderFeeless: SignedOrder; - let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder; - let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder; - let filledOpenSignedOrder: SignedOrder; - - const chainId = TESTRPC_CHAIN_ID; - const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI); - const partialFillAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI); - const takerFeeAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI); - - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - await blockchainLifecycle.startAsync(); - userAddresses = await web3Wrapper.getAvailableAddressesAsync(); - [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; - [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); - erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider); - erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider); - exchangeContract = new ExchangeContract(contractAddresses.exchange, provider); - - [makerAssetData, takerAssetData] = [ - assetDataUtils.encodeERC20AssetData(makerTokenAddress), - assetDataUtils.encodeERC20AssetData(takerTokenAddress), - ]; - - // Configure order defaults - const defaultOrderParams = { - ...devConstants.STATIC_ORDER_PARAMS, - makerAddress, - takerAddress: constants.NULL_ADDRESS, - makerAssetData, - takerAssetData, - makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - feeRecipientAddress: feeRecipient, - exchangeAddress: contractAddresses.exchange, - chainId, - }; - const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - - expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({ - expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).minus(10), - }); - - invalidSignatureOpenSignedOrder = await orderFactory.newSignedOrderAsync({ - takerAddress, - }); - invalidSignatureOpenSignedOrder.signature = expiredOpenSignedOrder.signature; - - fullyFillableOpenSignedOrder = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - }); - - // give double fillableAmount to maker and taker as buffer - await erc20MakerTokenContract - .transfer(makerAddress, fillableAmount.multipliedBy(4)) - .sendTransactionAsync({ from: coinbaseAddress }); - await erc20TakerTokenContract - .transfer(takerAddress, fillableAmount.multipliedBy(4)) - .sendTransactionAsync({ from: coinbaseAddress }); - await erc20MakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS) - .sendTransactionAsync({ from: makerAddress }); - await erc20MakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS) - .sendTransactionAsync({ from: takerAddress }); - await erc20TakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS) - .sendTransactionAsync({ from: takerAddress }); - - partiallyFilledOpenSignedOrderFeeless = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - }); - - await exchangeContract - .fillOrKillOrder( - partiallyFilledOpenSignedOrderFeeless, - partialFillAmount, - partiallyFilledOpenSignedOrderFeeless.signature, - ) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - - partiallyFilledOpenSignedOrderFeeInTakerAsset = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - takerFee: takerFeeAmount, - takerFeeAssetData: takerAssetData, - }); - - await exchangeContract - .fillOrKillOrder( - partiallyFilledOpenSignedOrderFeeInTakerAsset, - partialFillAmount, - partiallyFilledOpenSignedOrderFeeInTakerAsset.signature, - ) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - - partiallyFilledOpenSignedOrderFeeInMakerAsset = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - takerFee: takerFeeAmount, - takerFeeAssetData: makerAssetData, - }); - - await exchangeContract - .fillOrKillOrder( - partiallyFilledOpenSignedOrderFeeInMakerAsset, - partialFillAmount, - partiallyFilledOpenSignedOrderFeeInMakerAsset.signature, - ) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - - filledOpenSignedOrder = await orderFactory.newSignedOrderAsync({ - takerAssetAmount: fillableAmount, - makerAssetAmount: fillableAmount, - }); - - await exchangeContract - .fillOrKillOrder(filledOpenSignedOrder, fillableAmount, filledOpenSignedOrder.signature) - .sendTransactionAsync({ - from: takerAddress, - gasPrice: GAS_PRICE, - gas: 4000000, - value: PROTOCOL_FEE_PER_FILL, - }); - - orderStateUtils = new OrderStateUtils(new DevUtilsContract(contractAddresses.devUtils, provider)); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('#getSignedOrdersWithFillableAmountsAsync', () => { - it('should 0 fillableTakerAssetAmount for expired orders', async () => { - const orders = [expiredOpenSignedOrder]; - const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders); - isSignedOrdersWithFillableAmountsNotFillable(resultOrders); - }); - it('should filter out invalid signature orders', async () => { - const orders = [invalidSignatureOpenSignedOrder]; - const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders); - isSignedOrdersWithFillableAmountsNotFillable(resultOrders); - }); - it('should return 0 fillableTakerAssetAmount for fully filled orders', async () => { - const orders = [filledOpenSignedOrder]; - const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders); - isSignedOrdersWithFillableAmountsNotFillable(resultOrders); - }); - it('should provide correct pruned signed orders for fully fillable orders', async () => { - const orders = [fullyFillableOpenSignedOrder]; - const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders); - const order = resultOrders[0]; - expect(order.fillableMakerAssetAmount).to.bignumber.equal(fillableAmount); - expect(order.fillableTakerAssetAmount).to.bignumber.equal(fillableAmount); - }); - it('should provide correct pruned signed orders for partially fillable orders', async () => { - const orders = [ - partiallyFilledOpenSignedOrderFeeless, - partiallyFilledOpenSignedOrderFeeInTakerAsset, - partiallyFilledOpenSignedOrderFeeInMakerAsset, - ]; - const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders); - expect(resultOrders[0].fillableMakerAssetAmount).to.bignumber.equal( - fillableAmount.minus(partialFillAmount), - ); - expect(resultOrders[0].fillableTakerAssetAmount).to.bignumber.equal( - fillableAmount.minus(partialFillAmount), - ); - expect(resultOrders[1].fillableMakerAssetAmount).to.bignumber.equal( - fillableAmount.minus(partialFillAmount), - ); - expect(resultOrders[1].fillableTakerAssetAmount).to.bignumber.equal( - fillableAmount.minus(partialFillAmount), - ); - expect(resultOrders[1].fillableTakerFeeAmount).to.bignumber.equal( - new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI), - ); - expect(resultOrders[2].fillableMakerAssetAmount).to.bignumber.equal( - fillableAmount.minus(partialFillAmount), - ); - expect(resultOrders[2].fillableTakerAssetAmount).to.bignumber.equal( - fillableAmount.minus(partialFillAmount), - ); - expect(resultOrders[2].fillableTakerFeeAmount).to.bignumber.equal( - new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - }); -}); diff --git a/packages/asset-swapper/test/quote_report_generator_test.ts b/packages/asset-swapper/test/quote_report_generator_test.ts index 6c21a5439e..d0e5e0758f 100644 --- a/packages/asset-swapper/test/quote_report_generator_test.ts +++ b/packages/asset-swapper/test/quote_report_generator_test.ts @@ -1,52 +1,58 @@ // tslint:disable:custom-no-magic-numbers -import { SignedOrder } from '@0x/types'; +// tslint:disable:no-object-literal-type-assertion +import { FillQuoteTransformerOrderType, LimitOrder, LimitOrderFields, RfqOrder } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; import 'mocha'; import * as TypeMoq from 'typemoq'; -import { MarketOperation } from '../src/types'; +import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types'; import { CollapsedFill, DexSample, ERC20BridgeSource, MultiHopFillData, NativeCollapsedFill, + NativeFillData, + NativeLimitOrderFillData, + NativeRfqOrderFillData, } from '../src/utils/market_operation_utils/types'; import { QuoteRequestor } from '../src/utils/quote_requestor'; import { - BridgeReportSource, + BridgeQuoteReportEntry, generateQuoteReport, - MultiHopReportSource, - NativeOrderbookReportSource, - NativeRFQTReportSource, - QuoteReportSource, + MultiHopQuoteReportEntry, + NativeLimitOrderQuoteReportEntry, + NativeRfqOrderQuoteReportEntry, + QuoteReportEntry, } from './../src/utils/quote_report_generator'; import { chaiSetup } from './utils/chai_setup'; -import { testOrderFactory } from './utils/test_order_factory'; +import { getRandomAmount, getRandomSignature } from './utils/utils'; chaiSetup.configure(); const expect = chai.expect; -const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill => { +function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): NativeCollapsedFill { + const fillData = { + order: order.order, + signature: order.signature, + maxTakerTokenFillAmount: order.fillableTakerAmount, + }; return { sourcePathId: hexUtils.random(), source: ERC20BridgeSource.Native, - input: order.takerAssetAmount, - output: order.makerAssetAmount, - fillData: { - order: { - ...order, - fillableMakerAssetAmount: new BigNumber(1), - fillableTakerAssetAmount: new BigNumber(1), - fillableTakerFeeAmount: new BigNumber(1), - }, - }, + type: order.type, + input: order.order.takerAmount, + output: order.order.makerAmount, + fillData: + order.type === FillQuoteTransformerOrderType.Limit + ? (fillData as NativeLimitOrderFillData) + : (fillData as NativeRfqOrderFillData), subFills: [], }; -}; +} describe('generateQuoteReport', async () => { it('should generate report properly for sell', () => { @@ -78,37 +84,59 @@ describe('generateQuoteReport', async () => { }; const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2]; - const orderbookOrder1FillableAmount = new BigNumber(1000); - const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ - signature: 'orderbookOrder1', - takerAssetAmount: orderbookOrder1FillableAmount, - }); - const orderbookOrder2FillableAmount = new BigNumber(99); - const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({ - signature: 'orderbookOrder2', - takerAssetAmount: orderbookOrder2FillableAmount.plus(99), - }); - const rfqtOrder1FillableAmount = new BigNumber(100); - const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({ - signature: 'rfqtOrder1', - takerAssetAmount: rfqtOrder1FillableAmount, - }); - const rfqtOrder2FillableAmount = new BigNumber(1001); - const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({ - signature: 'rfqtOrder2', - takerAssetAmount: rfqtOrder2FillableAmount.plus(100), - }); - const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2]; - const orderFillableAmounts: BigNumber[] = [ - orderbookOrder1FillableAmount, - rfqtOrder1FillableAmount, - rfqtOrder2FillableAmount, - orderbookOrder2FillableAmount, + const orderbookOrder1: NativeOrderWithFillableAmounts = { + order: new LimitOrder({ takerAmount: new BigNumber(1000) }), + type: FillQuoteTransformerOrderType.Limit, + fillableTakerAmount: new BigNumber(1000), + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; + const orderbookOrder2: NativeOrderWithFillableAmounts = { + order: new LimitOrder({ takerAmount: new BigNumber(198) }), + type: FillQuoteTransformerOrderType.Limit, + fillableTakerAmount: new BigNumber(99), // takerAmount minus 99 + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; + const rfqtOrder1: NativeOrderWithFillableAmounts = { + order: new RfqOrder({ takerAmount: new BigNumber(100) }), + type: FillQuoteTransformerOrderType.Rfq, + fillableTakerAmount: new BigNumber(100), + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; + const rfqtOrder2: NativeOrderWithFillableAmounts = { + order: new RfqOrder({ takerAmount: new BigNumber(1101) }), + type: FillQuoteTransformerOrderType.Rfq, + fillableTakerAmount: new BigNumber(1001), + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; + + const nativeOrders: NativeOrderWithFillableAmounts[] = [ + orderbookOrder1, + rfqtOrder1, + rfqtOrder2, + orderbookOrder2, ]; // generate path - const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [], sourcePathId: hexUtils.random() }; - const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [], sourcePathId: hexUtils.random() }; + const uniswap2Fill: CollapsedFill = { + ...uniswapSample2, + subFills: [], + sourcePathId: hexUtils.random(), + type: FillQuoteTransformerOrderType.Bridge, + }; + const kyber2Fill: CollapsedFill = { + ...kyberSample2, + subFills: [], + sourcePathId: hexUtils.random(), + type: FillQuoteTransformerOrderType.Bridge, + }; const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2); const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2); const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill]; @@ -116,19 +144,13 @@ describe('generateQuoteReport', async () => { // quote generator mock const quoteRequestor = TypeMoq.Mock.ofType(); quoteRequestor - .setup(qr => qr.getMakerUriForOrderSignature(orderbookOrder2.signature)) - .returns(() => { - return undefined; - }) - .verifiable(TypeMoq.Times.atLeastOnce()); - quoteRequestor - .setup(qr => qr.getMakerUriForOrderSignature(rfqtOrder1.signature)) + .setup(qr => qr.getMakerUriForSignature(rfqtOrder1.signature)) .returns(() => { return 'https://rfqt1.provider.club'; }) .verifiable(TypeMoq.Times.atLeastOnce()); quoteRequestor - .setup(qr => qr.getMakerUriForOrderSignature(rfqtOrder2.signature)) + .setup(qr => qr.getMakerUriForSignature(rfqtOrder2.signature)) .returns(() => { return 'https://rfqt2.provider.club'; }) @@ -139,72 +161,79 @@ describe('generateQuoteReport', async () => { dexQuotes, [], nativeOrders, - orderFillableAmounts, pathGenerated, undefined, quoteRequestor.object, ); - const rfqtOrder1Source: NativeRFQTReportSource = { + const rfqtOrder1Source: NativeRfqOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: rfqtOrder1.makerAssetAmount, - takerAmount: rfqtOrder1.takerAssetAmount, - nativeOrder: rfqtOrder1, - fillableTakerAmount: rfqtOrder1FillableAmount, + makerAmount: rfqtOrder1.order.makerAmount, + takerAmount: rfqtOrder1.order.takerAmount, + fillableTakerAmount: rfqtOrder1.fillableTakerAmount, isRfqt: true, makerUri: 'https://rfqt1.provider.club', + fillData: { + order: rfqtOrder1.order, + } as NativeRfqOrderFillData, }; - const rfqtOrder2Source: NativeRFQTReportSource = { + const rfqtOrder2Source: NativeRfqOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: rfqtOrder2.makerAssetAmount, - takerAmount: rfqtOrder2.takerAssetAmount, - nativeOrder: rfqtOrder2, - fillableTakerAmount: rfqtOrder2FillableAmount, + makerAmount: rfqtOrder2.order.makerAmount, + takerAmount: rfqtOrder2.order.takerAmount, + fillableTakerAmount: rfqtOrder2.fillableTakerAmount, isRfqt: true, makerUri: 'https://rfqt2.provider.club', + fillData: { + order: rfqtOrder2.order, + } as NativeRfqOrderFillData, }; - const orderbookOrder1Source: NativeOrderbookReportSource = { + const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: orderbookOrder1.makerAssetAmount, - takerAmount: orderbookOrder1.takerAssetAmount, - nativeOrder: orderbookOrder1, - fillableTakerAmount: orderbookOrder1FillableAmount, + makerAmount: orderbookOrder1.order.makerAmount, + takerAmount: orderbookOrder1.order.takerAmount, + fillableTakerAmount: orderbookOrder1.fillableTakerAmount, isRfqt: false, + fillData: { + order: orderbookOrder1.order, + } as NativeLimitOrderFillData, }; - const orderbookOrder2Source: NativeOrderbookReportSource = { + const orderbookOrder2Source: NativeLimitOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: orderbookOrder2.makerAssetAmount, - takerAmount: orderbookOrder2.takerAssetAmount, - nativeOrder: orderbookOrder2, - fillableTakerAmount: orderbookOrder2FillableAmount, + makerAmount: orderbookOrder2.order.makerAmount, + takerAmount: orderbookOrder2.order.takerAmount, + fillableTakerAmount: orderbookOrder2.fillableTakerAmount, isRfqt: false, + fillData: { + order: orderbookOrder2.order, + } as NativeLimitOrderFillData, }; - const uniswap1Source: BridgeReportSource = { + const uniswap1Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.UniswapV2, makerAmount: uniswapSample1.output, takerAmount: uniswapSample1.input, fillData: {}, }; - const uniswap2Source: BridgeReportSource = { + const uniswap2Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.UniswapV2, makerAmount: uniswapSample2.output, takerAmount: uniswapSample2.input, fillData: {}, }; - const kyber1Source: BridgeReportSource = { + const kyber1Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample1.output, takerAmount: kyberSample1.input, fillData: {}, }; - const kyber2Source: BridgeReportSource = { + const kyber2Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample2.output, takerAmount: kyberSample2.input, fillData: {}, }; - const expectedSourcesConsidered: QuoteReportSource[] = [ + const expectedSourcesConsidered: QuoteReportEntry[] = [ kyber1Source, kyber2Source, uniswap1Source, @@ -214,39 +243,14 @@ describe('generateQuoteReport', async () => { rfqtOrder2Source, orderbookOrder2Source, ]; - - expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); - - orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { - const expectedSourceConsidered = expectedSourcesConsidered[idx]; - expect(actualSourcesConsidered).to.eql( - expectedSourceConsidered, - `sourceConsidered incorrect at index ${idx}`, - ); - }); - - const expectedSourcesDelivered: QuoteReportSource[] = [ + const expectedSourcesDelivered: QuoteReportEntry[] = [ rfqtOrder2Source, orderbookOrder2Source, uniswap2Source, kyber2Source, ]; - expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); - orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { - const expectedSourceDelivered = expectedSourcesDelivered[idx]; - - // remove fillable values - if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { - actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ - 'fillableMakerAssetAmount', - 'fillableTakerAssetAmount', - 'fillableTakerFeeAmount', - ]) as SignedOrder; - } - - expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`); - }); - + expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`); + expectEqualQuoteReportEntries(orderReport.sourcesDelivered, expectedSourcesDelivered, `sourcesDelivered`); quoteRequestor.verifyAll(); }); it('should handle properly for buy without quoteRequestor', () => { @@ -264,95 +268,84 @@ describe('generateQuoteReport', async () => { fillData: {}, }; const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1]; - - const orderbookOrder1FillableAmount = new BigNumber(1000); - const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ - signature: 'orderbookOrder1', - takerAssetAmount: orderbookOrder1FillableAmount.plus(101), - }); - const orderbookOrder2FillableAmount = new BigNumber(5000); - const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({ - signature: 'orderbookOrder2', - takerAssetAmount: orderbookOrder2FillableAmount.plus(101), - }); - const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2]; - const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount]; + const orderbookOrder1: NativeOrderWithFillableAmounts = { + order: new LimitOrder({ takerAmount: new BigNumber(1101) }), + type: FillQuoteTransformerOrderType.Limit, + fillableTakerAmount: new BigNumber(1000), + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; + const orderbookOrder2: NativeOrderWithFillableAmounts = { + order: new LimitOrder({ takerAmount: new BigNumber(5101) }), + type: FillQuoteTransformerOrderType.Limit, + fillableTakerAmount: new BigNumber(5000), // takerAmount minus 99 + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; + const nativeOrders = [orderbookOrder1, orderbookOrder2]; // generate path const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1); - const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [], sourcePathId: hexUtils.random() }; - const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() }; + const uniswap1Fill: CollapsedFill = { + ...uniswapSample1, + subFills: [], + sourcePathId: hexUtils.random(), + type: FillQuoteTransformerOrderType.Bridge, + }; + const kyber1Fill: CollapsedFill = { + ...kyberSample1, + subFills: [], + sourcePathId: hexUtils.random(), + type: FillQuoteTransformerOrderType.Bridge, + }; const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill]; - const orderReport = generateQuoteReport( - marketOperation, - dexQuotes, - [], - nativeOrders, - orderFillableAmounts, - pathGenerated, - ); + const orderReport = generateQuoteReport(marketOperation, dexQuotes, [], nativeOrders, pathGenerated); - const orderbookOrder1Source: NativeOrderbookReportSource = { + const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: orderbookOrder1.makerAssetAmount, - takerAmount: orderbookOrder1.takerAssetAmount, - nativeOrder: orderbookOrder1, - fillableTakerAmount: orderbookOrder1FillableAmount, + makerAmount: orderbookOrder1.order.makerAmount, + takerAmount: orderbookOrder1.order.takerAmount, + fillableTakerAmount: orderbookOrder1.fillableTakerAmount, isRfqt: false, + fillData: { + order: orderbookOrder1.order, + } as NativeLimitOrderFillData, }; - const orderbookOrder2Source: NativeOrderbookReportSource = { + const orderbookOrder2Source: NativeLimitOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: orderbookOrder2.makerAssetAmount, - takerAmount: orderbookOrder2.takerAssetAmount, - nativeOrder: orderbookOrder2, - fillableTakerAmount: orderbookOrder2FillableAmount, + makerAmount: orderbookOrder2.order.makerAmount, + takerAmount: orderbookOrder2.order.takerAmount, + fillableTakerAmount: orderbookOrder2.fillableTakerAmount, isRfqt: false, + fillData: { + order: orderbookOrder2.order, + } as NativeLimitOrderFillData, }; - const uniswap1Source: BridgeReportSource = { + const uniswap1Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.UniswapV2, makerAmount: uniswapSample1.input, takerAmount: uniswapSample1.output, fillData: {}, }; - const kyber1Source: BridgeReportSource = { + const kyber1Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample1.input, takerAmount: kyberSample1.output, fillData: {}, }; - const expectedSourcesConsidered: QuoteReportSource[] = [ + const expectedSourcesConsidered: QuoteReportEntry[] = [ kyber1Source, uniswap1Source, orderbookOrder1Source, orderbookOrder2Source, ]; - expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); - orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { - const expectedSourceConsidered = expectedSourcesConsidered[idx]; - expect(actualSourcesConsidered).to.eql( - expectedSourceConsidered, - `sourceConsidered incorrect at index ${idx}`, - ); - }); - - const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source]; - expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); - orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { - const expectedSourceDelivered = expectedSourcesDelivered[idx]; - - // remove fillable values - if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { - actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ - 'fillableMakerAssetAmount', - 'fillableTakerAssetAmount', - 'fillableTakerFeeAmount', - ]) as SignedOrder; - } - - expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`); - }); + const expectedSourcesDelivered: QuoteReportEntry[] = [orderbookOrder1Source, uniswap1Source, kyber1Source]; + expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`); + expectEqualQuoteReportEntries(orderReport.sourcesDelivered, expectedSourcesDelivered, `sourcesDelivered`); }); it('should correctly generate report for a two-hop quote', () => { const marketOperation: MarketOperation = MarketOperation.Sell; @@ -362,29 +355,31 @@ describe('generateQuoteReport', async () => { output: new BigNumber(10001), fillData: {}, }; - - const orderbookOrder1FillableAmount = new BigNumber(1000); - const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ - signature: 'orderbookOrder1', - takerAssetAmount: orderbookOrder1FillableAmount.plus(101), - }); - + const orderbookOrder1: NativeOrderWithFillableAmounts = { + order: new LimitOrder({ takerAmount: new BigNumber(1101) }), + type: FillQuoteTransformerOrderType.Limit, + fillableTakerAmount: new BigNumber(1000), + fillableMakerAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + signature: getRandomSignature(), + }; const twoHopFillData: MultiHopFillData = { intermediateToken: hexUtils.random(20), firstHopSource: { source: ERC20BridgeSource.Balancer, + fillData: {}, encodeCall: () => '', handleCallResults: _callResults => [new BigNumber(1337)], handleRevert: _c => [], }, secondHopSource: { source: ERC20BridgeSource.Curve, + fillData: {}, encodeCall: () => '', handleCallResults: _callResults => [new BigNumber(1337)], handleRevert: _c => [], }, }; - const twoHopSample: DexSample = { source: ERC20BridgeSource.MultiHop, input: new BigNumber(3005), @@ -397,24 +392,25 @@ describe('generateQuoteReport', async () => { [kyberSample1], [twoHopSample], [orderbookOrder1], - [orderbookOrder1FillableAmount], twoHopSample, ); - const orderbookOrder1Source: NativeOrderbookReportSource = { + const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Native, - makerAmount: orderbookOrder1.makerAssetAmount, - takerAmount: orderbookOrder1.takerAssetAmount, - nativeOrder: orderbookOrder1, - fillableTakerAmount: orderbookOrder1FillableAmount, + makerAmount: orderbookOrder1.order.makerAmount, + takerAmount: orderbookOrder1.order.takerAmount, + fillableTakerAmount: orderbookOrder1.fillableTakerAmount, isRfqt: false, + fillData: { + order: orderbookOrder1.order, + } as NativeLimitOrderFillData, }; - const kyber1Source: BridgeReportSource = { + const kyber1Source: BridgeQuoteReportEntry = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample1.output, takerAmount: kyberSample1.input, fillData: {}, }; - const twoHopSource: MultiHopReportSource = { + const twoHopSource: MultiHopQuoteReportEntry = { liquiditySource: ERC20BridgeSource.MultiHop, makerAmount: twoHopSample.output, takerAmount: twoHopSample.input, @@ -422,17 +418,37 @@ describe('generateQuoteReport', async () => { fillData: twoHopFillData, }; - const expectedSourcesConsidered: QuoteReportSource[] = [kyber1Source, orderbookOrder1Source, twoHopSource]; - expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); - orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { - const expectedSourceConsidered = expectedSourcesConsidered[idx]; - expect(actualSourcesConsidered).to.eql( - expectedSourceConsidered, - `sourceConsidered incorrect at index ${idx}`, - ); - }); - + const expectedSourcesConsidered: QuoteReportEntry[] = [kyber1Source, orderbookOrder1Source, twoHopSource]; + expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`); expect(orderReport.sourcesDelivered.length).to.eql(1); expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource); }); }); + +function expectEqualQuoteReportEntries( + actual: QuoteReportEntry[], + expected: QuoteReportEntry[], + variableName: string = 'quote report entries', +): void { + expect(actual.length).to.eql(expected.length); + actual.forEach((actualEntry, idx) => { + const expectedEntry = expected[idx]; + // remove fillable values + if (actualEntry.liquiditySource === ERC20BridgeSource.Native) { + actualEntry.fillData.order = _.omit(actualEntry.fillData.order, [ + 'fillableMakerAmount', + 'fillableTakerAmount', + 'fillableTakerFeeAmount', + ]) as LimitOrderFields; + expect(actualEntry.fillData.order).to.eql( + // tslint:disable-next-line:no-unnecessary-type-assertion + (expectedEntry.fillData as NativeFillData).order, + `${variableName} incorrect at index ${idx}`, + ); + } + expect(_.omit(actualEntry, 'fillData')).to.eql( + _.omit(expectedEntry, 'fillData'), + `${variableName} incorrect at index ${idx}`, + ); + }); +} diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts index e76946eef0..12d3d05ffb 100644 --- a/packages/asset-swapper/test/quote_requestor_test.ts +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -1,18 +1,19 @@ import { tokenUtils } from '@0x/dev-utils'; -import { assetDataUtils } from '@0x/order-utils'; +import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils'; import { TakerRequestQueryParams } from '@0x/quote-server'; import { StatusCodes } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; +import _ = require('lodash'); import 'mocha'; import { constants } from '../src/constants'; -import { MarketOperation, MockedRfqtFirmQuoteResponse, MockedRfqtIndicativeQuoteResponse } from '../src/types'; +import { MarketOperation, MockedRfqtQuoteResponse } from '../src/types'; +import { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants'; import { QuoteRequestor, quoteRequestorHttpClient } from '../src/utils/quote_requestor'; -import { rfqtMocker } from '../src/utils/rfqt_mocker'; import { chaiSetup } from './utils/chai_setup'; -import { testOrderFactory } from './utils/test_order_factory'; +import { RfqtQuoteEndpoint, testHelpers } from './utils/test_helpers'; chaiSetup.configure(); const expect = chai.expect; @@ -25,138 +26,120 @@ function makeThreeMinuteExpiry(): BigNumber { describe('QuoteRequestor', async () => { const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses(); - const makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken); - const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken); + const validSignature = { v: 28, r: '0x', s: '0x', signatureType: SignatureType.EthSign }; describe('requestRfqtFirmQuotesAsync for firm quotes', async () => { it('should return successful RFQT requests', async () => { const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; + const txOrigin = takerAddress; const apiKey = 'my-ko0l-api-key'; // Set up RFQT responses // tslint:disable-next-line:array-type - const mockedRequests: MockedRfqtFirmQuoteResponse[] = []; + const mockedRequests: MockedRfqtQuoteResponse[] = []; const expectedParams: TakerRequestQueryParams = { sellTokenAddress: takerToken, buyTokenAddress: makerToken, sellAmountBaseUnits: '10000', comparisonPrice: undefined, takerAddress, - protocolVersion: '3', + txOrigin, + protocolVersion: '4', }; - // Successful response - const successfulOrder1 = testOrderFactory.generateTestSignedOrder({ - makerAssetData, - takerAssetData, - takerAddress, - feeRecipientAddress: '0x0000000000000000000000000000000000000001', - expirationTimeSeconds: makeThreeMinuteExpiry(), - }); - mockedRequests.push({ - endpoint: 'https://1337.0.0.1', + const mockedDefaults = { requestApiKey: apiKey, requestParams: expectedParams, - responseData: { signedOrder: successfulOrder1 }, responseCode: StatusCodes.Success, + }; + const validSignedOrder = { + makerToken, + takerToken, + makerAmount: new BigNumber('1000'), + takerAmount: new BigNumber('1000'), + maker: takerAddress, + taker: takerAddress, + pool: '0x', + salt: '0', + chainId: 1, + verifyingContract: takerAddress, + txOrigin, + expiry: makeThreeMinuteExpiry(), + signature: validSignature, + }; + + // Successful response + mockedRequests.push({ + ...mockedDefaults, + endpoint: 'https://1337.0.0.1', + responseData: { + signedOrder: validSignedOrder, + }, + }); + // Another Successful response + mockedRequests.push({ + ...mockedDefaults, + endpoint: 'https://37.0.0.1', + responseData: { signedOrder: validSignedOrder }, }); // Test out a bad response code, ensure it doesnt cause throw mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://420.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, responseData: { error: 'bad request' }, responseCode: StatusCodes.InternalError, }); - // Test out a successful response code but an invalid order + // Test out a successful response code but a partial order mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://421.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { makerAssetData: '123' }, - responseCode: StatusCodes.Success, + responseData: { signedOrder: { makerToken: '123' } }, }); - // ensure that a non-JSON response doesn't throw an error when trying to parse + // A successful response code and invalid response data (encoding) mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://421.1.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, responseData: 'this is not JSON!', - responseCode: StatusCodes.Success, }); // A successful response code and valid order, but for wrong maker asset data - const wrongMakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({ - makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1), - expirationTimeSeconds: makeThreeMinuteExpiry(), - takerAssetData, - }); mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://422.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { signedOrder: wrongMakerAssetDataOrder }, - responseCode: StatusCodes.Success, + responseData: { signedOrder: { ...validSignedOrder, makerToken: '0x1234' } }, }); // A successful response code and valid order, but for wrong taker asset data - const wrongTakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({ - makerAssetData, - expirationTimeSeconds: makeThreeMinuteExpiry(), - takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1), - }); mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://423.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { signedOrder: wrongTakerAssetDataOrder }, - responseCode: StatusCodes.Success, + responseData: { signedOrder: { ...validSignedOrder, takerToken: '0x1234' } }, }); // A successful response code and good order but its unsigned - const unsignedOrder = testOrderFactory.generateTestSignedOrder({ - makerAssetData, - takerAssetData, - expirationTimeSeconds: makeThreeMinuteExpiry(), - feeRecipientAddress: '0x0000000000000000000000000000000000000002', - }); - delete unsignedOrder.signature; mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://424.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { signedOrder: unsignedOrder }, - responseCode: StatusCodes.Success, - }); - // A successful response code and good order but for the wrong takerAddress - const orderWithNullTaker = testOrderFactory.generateTestSignedOrder({ - makerAssetData, - takerAssetData, - expirationTimeSeconds: makeThreeMinuteExpiry(), - takerAddress: constants.NULL_ADDRESS, - feeRecipientAddress: '0x0000000000000000000000000000000000000002', + responseData: { signedOrder: _.omit(validSignedOrder, ['signature']) }, }); + // A successful response code and good order but for the wrong txOrigin mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://425.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { signedOrder: orderWithNullTaker }, - responseCode: StatusCodes.Success, + responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } }, }); - // Another Successful response - const successfulOrder2 = testOrderFactory.generateTestSignedOrder({ - makerAssetData, - takerAssetData, - takerAddress, - expirationTimeSeconds: makeThreeMinuteExpiry(), - }); - mockedRequests.push({ - endpoint: 'https://37.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { signedOrder: successfulOrder2 }, - responseCode: StatusCodes.Success, - }); + const normalizedSuccessfulOrder = { + order: { + ..._.omit(validSignedOrder, ['signature']), + makerAmount: new BigNumber(validSignedOrder.makerAmount), + takerAmount: new BigNumber(validSignedOrder.takerAmount), + expiry: new BigNumber(validSignedOrder.expiry), + salt: new BigNumber(validSignedOrder.salt), + }, + signature: validSignedOrder.signature, + type: FillQuoteTransformerOrderType.Rfq, + }; - return rfqtMocker.withMockedRfqtFirmQuotes( + return testHelpers.withMockedRfqtQuotes( mockedRequests, + RfqtQuoteEndpoint.Firm, async () => { const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]], @@ -167,25 +150,23 @@ describe('QuoteRequestor', async () => { 'https://423.0.0.1': [[makerToken, takerToken]], 'https://424.0.0.1': [[makerToken, takerToken]], 'https://425.0.0.1': [[makerToken, takerToken]], - 'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T - provider when they don't support the requested asset pair. */, + 'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. */, 'https://37.0.0.1': [[makerToken, takerToken]], }); const resp = await qr.requestRfqtFirmQuotesAsync( - makerAssetData, - takerAssetData, + makerToken, + takerToken, new BigNumber(10000), MarketOperation.Sell, undefined, { apiKey, takerAddress, + txOrigin: takerAddress, intentOnFilling: true, }, ); - expect(resp.sort()).to.eql( - [{ signedOrder: successfulOrder1 }, { signedOrder: successfulOrder2 }].sort(), - ); + expect(resp).to.deep.eq([normalizedSuccessfulOrder, normalizedSuccessfulOrder]); }, quoteRequestorHttpClient, ); @@ -194,10 +175,11 @@ describe('QuoteRequestor', async () => { describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => { it('should optionally accept a "comparisonPrice" parameter', async () => { const response = QuoteRequestor.makeQueryParameters( - otherToken1, + otherToken1, // tx origin + otherToken1, // taker MarketOperation.Sell, - makerAssetData, - takerAssetData, + makerToken, + takerToken, new BigNumber(1000), new BigNumber(300.2), ); @@ -209,73 +191,71 @@ describe('QuoteRequestor', async () => { // Set up RFQT responses // tslint:disable-next-line:array-type - const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; + const mockedRequests: MockedRfqtQuoteResponse[] = []; const expectedParams: TakerRequestQueryParams = { sellTokenAddress: takerToken, buyTokenAddress: makerToken, sellAmountBaseUnits: '10000', comparisonPrice: undefined, - protocolVersion: '3', takerAddress, + txOrigin: takerAddress, + protocolVersion: '4', }; - // Successful response - const successfulQuote1 = { - makerAssetData, - takerAssetData, - makerAssetAmount: new BigNumber(expectedParams.sellAmountBaseUnits), - takerAssetAmount: new BigNumber(expectedParams.sellAmountBaseUnits), - expirationTimeSeconds: makeThreeMinuteExpiry(), - }; - mockedRequests.push({ - endpoint: 'https://1337.0.0.1', + const mockedDefaults = { requestApiKey: apiKey, requestParams: expectedParams, - responseData: successfulQuote1, responseCode: StatusCodes.Success, + }; + + // Successful response + const successfulQuote1 = { + makerToken, + takerToken, + makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits), + takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits), + expiry: makeThreeMinuteExpiry(), + }; + + mockedRequests.push({ + ...mockedDefaults, + endpoint: 'https://1337.0.0.1', + responseData: successfulQuote1, }); // Test out a bad response code, ensure it doesnt cause throw mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://420.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, responseData: { error: 'bad request' }, responseCode: StatusCodes.InternalError, }); // Test out a successful response code but an invalid order mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://421.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { makerAssetData: '123' }, - responseCode: StatusCodes.Success, + responseData: { makerToken: '123' }, }); // A successful response code and valid response data, but for wrong maker asset data mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://422.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { ...successfulQuote1, makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) }, - responseCode: StatusCodes.Success, + responseData: { ...successfulQuote1, makerToken: otherToken1 }, }); // A successful response code and valid response data, but for wrong taker asset data mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://423.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, - responseData: { ...successfulQuote1, takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) }, - responseCode: StatusCodes.Success, + responseData: { ...successfulQuote1, takerToken: otherToken1 }, }); // Another Successful response mockedRequests.push({ + ...mockedDefaults, endpoint: 'https://37.0.0.1', - requestApiKey: apiKey, - requestParams: expectedParams, responseData: successfulQuote1, - responseCode: StatusCodes.Success, }); - return rfqtMocker.withMockedRfqtIndicativeQuotes( + return testHelpers.withMockedRfqtQuotes( mockedRequests, + RfqtQuoteEndpoint.Indicative, async () => { const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]], @@ -287,14 +267,15 @@ describe('QuoteRequestor', async () => { 'https://37.0.0.1': [[makerToken, takerToken]], }); const resp = await qr.requestRfqtIndicativeQuotesAsync( - makerAssetData, - takerAssetData, + makerToken, + takerToken, new BigNumber(10000), MarketOperation.Sell, undefined, { apiKey, takerAddress, + txOrigin: takerAddress, intentOnFilling: true, }, ); @@ -303,28 +284,29 @@ describe('QuoteRequestor', async () => { quoteRequestorHttpClient, ); }); - it('should return successful RFQT indicative quote requests', async () => { + it('should return successful RFQT indicative quote requests (Buy)', async () => { const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; const apiKey = 'my-ko0l-api-key'; // Set up RFQT responses // tslint:disable-next-line:array-type - const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; + const mockedRequests: MockedRfqtQuoteResponse[] = []; const expectedParams: TakerRequestQueryParams = { sellTokenAddress: takerToken, buyTokenAddress: makerToken, buyAmountBaseUnits: '10000', comparisonPrice: undefined, - protocolVersion: '3', takerAddress, + txOrigin: takerAddress, + protocolVersion: '4', }; // Successful response const successfulQuote1 = { - makerAssetData, - takerAssetData, - makerAssetAmount: new BigNumber(expectedParams.buyAmountBaseUnits), - takerAssetAmount: new BigNumber(expectedParams.buyAmountBaseUnits), - expirationTimeSeconds: makeThreeMinuteExpiry(), + makerToken, + takerToken, + makerAmount: new BigNumber(expectedParams.buyAmountBaseUnits), + takerAmount: new BigNumber(expectedParams.buyAmountBaseUnits), + expiry: makeThreeMinuteExpiry(), }; mockedRequests.push({ endpoint: 'https://1337.0.0.1', @@ -334,19 +316,21 @@ describe('QuoteRequestor', async () => { responseCode: StatusCodes.Success, }); - return rfqtMocker.withMockedRfqtIndicativeQuotes( + return testHelpers.withMockedRfqtQuotes( mockedRequests, + RfqtQuoteEndpoint.Indicative, async () => { const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] }); const resp = await qr.requestRfqtIndicativeQuotesAsync( - makerAssetData, - takerAssetData, + makerToken, + takerToken, new BigNumber(10000), MarketOperation.Buy, undefined, { apiKey, takerAddress, + txOrigin: takerAddress, intentOnFilling: true, }, ); diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts index ae106b30df..240cbb1527 100644 --- a/packages/asset-swapper/test/quote_simulation_test.ts +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -1,10 +1,16 @@ import { constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; -import { assetDataUtils } from '@0x/order-utils'; -import { BigNumber, hexUtils } from '@0x/utils'; +import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils'; +import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils'; import * as _ from 'lodash'; import { MarketOperation } from '../src/types'; -import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types'; +import { + CollapsedFill, + ERC20BridgeSource, + NativeLimitOrderFillData, + OptimizedMarketOrder, + OptimizedMarketOrderBase, +} from '../src/utils/market_operation_utils/types'; import { fillQuoteOrders, QuoteFillOrderCall, @@ -20,9 +26,7 @@ describe('quote_simulation tests', async () => { const ONE = new BigNumber(1); const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); - const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); - const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); - const GAS_SCHEDULE = { [ERC20BridgeSource.Uniswap]: _.constant(1) }; + const GAS_SCHEDULE = { [ERC20BridgeSource.Uniswap]: _.constant(1), [ERC20BridgeSource.Native]: _.constant(1) }; // Check if two numbers are within `maxError` error rate within each other. function assertRoughlyEquals(n1: BigNumber, n2: BigNumber, maxError: BigNumber | number = 1e-10): void { @@ -43,9 +47,10 @@ describe('quote_simulation tests', async () => { count: number; fillsCount: number; side: MarketOperation; + type?: FillQuoteTransformerOrderType; }> = {}, ): QuoteFillOrderCall[] { - const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side } = { + const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side, type } = { fillableInput: getRandomOrderSize(), fillableOutput: getRandomOrderSize(), inputFeeRate: 0, @@ -82,6 +87,7 @@ describe('quote_simulation tests', async () => { filledInput: filledInputs[i], takerInputFee: inputFees[i].abs(), takerOutputFee: outputFees[i].abs(), + type, }), totalOrderInput: totalInputs[i], totalOrderOutput: totalOutputs[i], @@ -100,60 +106,63 @@ describe('quote_simulation tests', async () => { side: MarketOperation; takerInputFee: BigNumber; takerOutputFee: BigNumber; + type: FillQuoteTransformerOrderType; }> = {}, - ): OptimizedMarketOrder { - const { filledInput, fillsCount, side, takerInputFee, takerOutputFee } = { - side: MarketOperation.Sell, - filledInput: ZERO, - fillsCount: 3, - takerInputFee: ZERO, - takerOutputFee: ZERO, - ...opts, - }; + ): OptimizedMarketOrderBase { + const { filledInput, fillsCount, side, takerInputFee, takerOutputFee, type } = _.merge( + {}, + { + side: MarketOperation.Sell, + filledInput: ZERO, + fillsCount: 3, + takerInputFee: ZERO, + takerOutputFee: ZERO, + type: FillQuoteTransformerOrderType.Limit, + }, + opts, + ); const filledOutput = filledInput .div(input) .times(output) .integerValue(BigNumber.ROUND_DOWN); const fillableInput = input.minus(filledInput); const fillableOutput = output.minus(filledOutput); - const makerAssetAmount = side === MarketOperation.Sell ? output : input; - const takerAssetAmount = side === MarketOperation.Sell ? input : output; - const fillableMakerAssetAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput; - const fillableTakerAssetAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput; + const makerAmount = side === MarketOperation.Sell ? output : input; + const takerAmount = side === MarketOperation.Sell ? input : output; + const fillableMakerAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput; + const fillableTakerAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput; const takerFee = BigNumber.max(takerInputFee, takerOutputFee); - let takerFeeAssetData = '0x'; - if (!takerInputFee.eq(0)) { - takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_TAKER_ASSET_DATA : DEFAULT_MAKER_ASSET_DATA; - } else if (!takerOutputFee.eq(0)) { - takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_MAKER_ASSET_DATA : DEFAULT_TAKER_ASSET_DATA; - } - const fillableTakerFeeAmount = fillableTakerAssetAmount - .div(takerAssetAmount) - .times(takerFee) - .integerValue(BigNumber.ROUND_DOWN); - return { - makerAssetAmount, - takerAssetAmount, - fillableTakerAssetAmount, - fillableMakerAssetAmount, - fillableTakerFeeAmount, - takerFee, - takerFeeAssetData, + + const order: OptimizedMarketOrderBase = { + source: ERC20BridgeSource.Native, + makerToken: MAKER_TOKEN, + takerToken: TAKER_TOKEN, + makerAmount: fillableMakerAmount, + takerAmount: fillableTakerAmount, + fillData: { + order: { + makerToken: MAKER_TOKEN, + makerAmount, + takerToken: TAKER_TOKEN, + takerAmount, + maker: NULL_ADDRESS, + taker: NULL_ADDRESS, + sender: NULL_ADDRESS, + salt: ZERO, + chainId: 1, + pool: NULL_BYTES, + verifyingContract: NULL_ADDRESS, + expiry: ZERO, + feeRecipient: NULL_ADDRESS, + takerTokenFeeAmount: takerFee, + }, + signature: { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign }, + maxTakerTokenFillAmount: fillableTakerAmount, + }, + type, fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount), - chainId: 1, - exchangeAddress: NULL_ADDRESS, - expirationTimeSeconds: ZERO, - feeRecipientAddress: NULL_ADDRESS, - senderAddress: NULL_ADDRESS, - makerAddress: NULL_ADDRESS, - takerAddress: NULL_ADDRESS, - makerAssetData: DEFAULT_MAKER_ASSET_DATA, - takerAssetData: DEFAULT_TAKER_ASSET_DATA, - makerFeeAssetData: '0x', - salt: ZERO, - makerFee: ZERO, - signature: '0x', }; + return order; } const nativeSourcePathId = hexUtils.random(); function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] { @@ -163,8 +172,10 @@ describe('quote_simulation tests', async () => { const subFillInputs = subdivideAmount(inputs[i], count); const subFillOutputs = subdivideAmount(outputs[i], count); return { + type: FillQuoteTransformerOrderType.Bridge, sourcePathId: nativeSourcePathId, source: ERC20BridgeSource.Uniswap, + fillData: {}, input: inputs[i], output: outputs[i], subFills: _.times(count, j => ({ @@ -475,6 +486,28 @@ describe('quote_simulation tests', async () => { expect(result.protocolFee).to.bignumber.eq(1); expect(result.gas).to.eq(fillsCount); }); + + it('does not charge a protocol fee for rfq orders', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + fillsCount, + count: 1, + type: FillQuoteTransformerOrderType.Rfq, + }); + const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(fillableInput); + assertRoughlyEquals(totalFilledOutput, fillableOutput); + expect(result.protocolFee).to.bignumber.eq(0); + expect(result.gas).to.eq(fillsCount); + }); }); describe('multiple orders', () => { @@ -666,18 +699,29 @@ describe('quote_simulation tests', async () => { }); function slipOrder( - order: OptimizedMarketOrder, + order: OptimizedMarketOrderBase, orderSlippage: number, side: MarketOperation, ): OptimizedMarketOrder { const makerScaling = side === MarketOperation.Sell ? 1 - orderSlippage : 1; const takerScaling = side === MarketOperation.Sell ? 1 : orderSlippage + 1; + + // tslint:disable:next-line no-unnecessary-type-assertion + const nativeFillData = order.fillData!; + const slippedFillData = { + order: { + ...nativeFillData.order, + takerAmount: nativeFillData.order.takerAmount.times(takerScaling), + makerAmount: nativeFillData.order.makerAmount.times(makerScaling), + }, + signature: nativeFillData.signature, + maxTakerTokenFillAmount: nativeFillData.maxTakerTokenFillAmount.times(takerScaling), + }; return { ...order, - makerAssetAmount: order.makerAssetAmount.times(makerScaling), - fillableMakerAssetAmount: order.fillableMakerAssetAmount.times(makerScaling), - takerAssetAmount: order.takerAssetAmount.times(takerScaling), - fillableTakerAssetAmount: order.fillableTakerAssetAmount.times(takerScaling), + makerAmount: order.makerAmount.times(makerScaling), + takerAmount: order.takerAmount.times(takerScaling), + fillData: slippedFillData, }; } @@ -687,11 +731,14 @@ describe('quote_simulation tests', async () => { const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const orderSlippage = getRandomFeeRate(); - const orders = createQuoteFillOrders({ + const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - }).map(fo => slipOrder(fo.order, orderSlippage, side)); + }); + const orders = fillOrders.map(fo => + slipOrder(fo.order as OptimizedMarketOrderBase, orderSlippage, side), + ); const result = simulateBestCaseFill({ orders, side, @@ -771,8 +818,8 @@ describe('quote_simulation tests', async () => { } }); - it('can fully fill orders with input fees', async () => { - const side = randomSide(); + it('can fully fill sell orders with "input" fees', async () => { + const side = MarketOperation.Sell; const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const inputFeeRate = getRandomFeeRate(); @@ -780,9 +827,8 @@ describe('quote_simulation tests', async () => { fillableInput, fillableOutput, inputFeeRate, - side, }).map(fo => fo.order); - const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const signedInputFeeRate = inputFeeRate; const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); const result = simulateBestCaseFill({ orders, @@ -791,27 +837,17 @@ describe('quote_simulation tests', async () => { gasPrice: ONE, opts: { gasSchedule: GAS_SCHEDULE }, }); - expect(result.gas).to.eq(countCollapsedFills(orders)); - expect(result.protocolFeeAmount).to.bignumber.gt(orders.length); - if (side === MarketOperation.Sell) { - assertRoughlyEquals(result.takerAssetAmount, fillableInput); - assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput); - assertRoughlyEquals(result.makerAssetAmount, fillableOutput); - assertRoughlyEquals(result.totalMakerAssetAmount, fillableOutput); - expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); - expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); - } else { - assertRoughlyEquals(result.makerAssetAmount, fillableInput); - assertRoughlyEquals(result.totalMakerAssetAmount, totalFillableInput); - assertRoughlyEquals(result.takerAssetAmount, fillableOutput); - assertRoughlyEquals(result.totalTakerAssetAmount, fillableOutput); - expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); - expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); - } + + assertRoughlyEquals(result.takerAssetAmount, fillableInput); + assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput); + assertRoughlyEquals(result.makerAssetAmount, fillableOutput); + assertRoughlyEquals(result.totalMakerAssetAmount, fillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); }); - it('can partially fill orders with input fees', async () => { - const side = randomSide(); + it('can partially fill sell orders with "input" fees', async () => { + const side = MarketOperation.Sell; const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const inputFeeRate = getRandomFeeRate(); @@ -821,7 +857,7 @@ describe('quote_simulation tests', async () => { inputFeeRate, side, }).map(fo => fo.order); - const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const signedInputFeeRate = inputFeeRate; const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); const inputFillAmount = totalFillableInput.times(2 / 3).integerValue(); const result = simulateBestCaseFill({ @@ -833,21 +869,14 @@ describe('quote_simulation tests', async () => { }); expect(result.gas).to.gt(0); expect(result.protocolFeeAmount).to.bignumber.gt(0); - if (side === MarketOperation.Sell) { - assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount); - expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); - expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); - expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); - } else { - assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount); - expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput); - expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); - expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); - } + assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount); + expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); }); - it('can fully fill orders with output fees', async () => { - const side = randomSide(); + it('can fully fill buy orders with "output" fees', async () => { + const side = MarketOperation.Buy; const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const outputFeeRate = getRandomFeeRate(); @@ -857,7 +886,7 @@ describe('quote_simulation tests', async () => { outputFeeRate, side, }).map(fo => fo.order); - const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const signedOutputFeeRate = outputFeeRate; const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); const result = simulateBestCaseFill({ orders, @@ -868,25 +897,17 @@ describe('quote_simulation tests', async () => { }); expect(result.gas).to.eq(countCollapsedFills(orders)); expect(result.protocolFeeAmount).to.bignumber.gt(orders.length); - if (side === MarketOperation.Sell) { - assertRoughlyEquals(result.takerAssetAmount, fillableInput); - assertRoughlyEquals(result.totalTakerAssetAmount, fillableInput); - assertRoughlyEquals(result.makerAssetAmount, fillableOutput); - assertRoughlyEquals(result.totalMakerAssetAmount, totalFillableOutput); - expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); - expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); - } else { - assertRoughlyEquals(result.makerAssetAmount, fillableInput); - assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput); - assertRoughlyEquals(result.takerAssetAmount, fillableOutput); - assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput); - expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); - expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); - } + + assertRoughlyEquals(result.makerAssetAmount, fillableInput); + assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput); + assertRoughlyEquals(result.takerAssetAmount, fillableOutput); + assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); }); - it('can partially fill orders with output fees', async () => { - const side = randomSide(); + it('can partially fill buy orders with "output" fees', async () => { + const side = MarketOperation.Buy; const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const outputFeeRate = getRandomFeeRate(); @@ -906,17 +927,10 @@ describe('quote_simulation tests', async () => { }); expect(result.gas).to.gt(0); expect(result.protocolFeeAmount).to.bignumber.gt(0); - if (side === MarketOperation.Sell) { - assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount); - expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); - expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); - expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); - } else { - assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount); - expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput); - expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); - expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); - } + assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount); + expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); }); }); @@ -925,25 +939,26 @@ describe('quote_simulation tests', async () => { const side = randomSide(); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); - const orderSlippage = getRandomFeeRate(); + const slippage = getRandomFeeRate(); const orders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - }).map(fo => slipOrder(fo.order, orderSlippage, side)); + }).map(fo => fo.order); + const result = simulateWorstCaseFill({ orders, side, fillAmount: fillableInput, gasPrice: ONE, - opts: { gasSchedule: GAS_SCHEDULE }, + opts: { gasSchedule: GAS_SCHEDULE, slippage }, }); if (side === MarketOperation.Sell) { - const slippedOutput = fillableOutput.times(1 - orderSlippage).integerValue(); + const slippedOutput = fillableOutput.times(1 - slippage).integerValue(); assertRoughlyEquals(result.totalMakerAssetAmount, slippedOutput); assertRoughlyEquals(result.totalTakerAssetAmount, fillableInput); } else { - const slippedOutput = fillableOutput.times(orderSlippage + 1).integerValue(); + const slippedOutput = fillableOutput.times(slippage + 1).integerValue(); assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput); assertRoughlyEquals(result.totalTakerAssetAmount, slippedOutput); } @@ -958,7 +973,9 @@ describe('quote_simulation tests', async () => { fillableInput, fillableOutput, side, - }).map(fo => slipOrder(fo.order, orderSlippage, side)); + }).map(fo => + slipOrder(fo.order as OptimizedMarketOrderBase, orderSlippage, side), + ); orders = [...orders.slice(1), orders[0]]; const bestCase = simulateBestCaseFill({ orders, @@ -972,7 +989,7 @@ describe('quote_simulation tests', async () => { side, fillAmount: fillableInput, gasPrice: ONE, - opts: { gasSchedule: GAS_SCHEDULE }, + opts: { gasSchedule: GAS_SCHEDULE, slippage: orderSlippage }, }); const bestPrice = bestCase.makerAssetAmount.div(bestCase.totalTakerAssetAmount); const worstPrice = worstCase.makerAssetAmount.div(worstCase.totalTakerAssetAmount); diff --git a/packages/asset-swapper/test/sorting_utils_test.ts b/packages/asset-swapper/test/sorting_utils_test.ts deleted file mode 100644 index e794ea303d..0000000000 --- a/packages/asset-swapper/test/sorting_utils_test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { sortingUtils } from '../src/utils/sorting_utils'; - -import { chaiSetup } from './utils/chai_setup'; -import { testOrderFactory } from './utils/test_order_factory'; - -chaiSetup.configure(); -const expect = chai.expect; - -const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b02222222222222222222222222222222222222222222222222222222222222222'; -const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b01111111111111111111111111111111111111111111111111111111111111111'; - -describe('sortingUtils', () => { - describe('#sortOrders', () => { - // rate: 2 takerAsset / makerAsset - const testOrder1 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(200), - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 1 takerAsset / makerAsset - const testOrder2 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(100), - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 2.5 takerAsset / makerAsset - const testOrder3 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(250), - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 2 takerAsset / makerAsset - const testOrderWithFeeInTakerAsset1 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(100), - takerFee: new BigNumber(100), - takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 1 takerAsset / makerAsset - const testOrderWithFeeInTakerAsset2 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(50), - takerFee: new BigNumber(50), - takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 2.5 takerAsset / makerAsset - const testOrderWithFeeInTakerAsset3 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(100), - takerAssetAmount: new BigNumber(200), - takerFee: new BigNumber(50), - takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 2 takerAsset / makerAsset - const testOrderWithFeeInMakerAsset1 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(200), - takerAssetAmount: new BigNumber(200), - takerFee: new BigNumber(100), - takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 1 takerAsset / makerAsset - const testOrderWithFeeInMakerAsset2 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(150), - takerAssetAmount: new BigNumber(100), - takerFee: new BigNumber(50), - takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - // rate: 2.5 takerAsset / makerAsset - const testOrderWithFeeInMakerAsset3 = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: new BigNumber(150), - takerAssetAmount: new BigNumber(250), - takerFee: new BigNumber(50), - takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }); - it('correctly sorts by fee adjusted rate (feeless orders)', async () => { - const orders = [testOrder1, testOrder2, testOrder3]; - const sortedOrders = sortingUtils.sortOrders(orders); - expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]); - }); - it('correctly sorts by fee adjusted rate (takerAsset denominated fee orders)', async () => { - const orders = [ - testOrderWithFeeInTakerAsset1, - testOrderWithFeeInTakerAsset2, - testOrderWithFeeInTakerAsset3, - ]; - const sortedOrders = sortingUtils.sortOrders(orders); - expect(sortedOrders).to.deep.equal([ - testOrderWithFeeInTakerAsset2, - testOrderWithFeeInTakerAsset1, - testOrderWithFeeInTakerAsset3, - ]); - }); - it('correctly sorts by fee adjusted rate (makerAsset denominated fee orders)', async () => { - const orders = [ - testOrderWithFeeInMakerAsset1, - testOrderWithFeeInMakerAsset2, - testOrderWithFeeInMakerAsset3, - ]; - const sortedOrders = sortingUtils.sortOrders(orders); - expect(sortedOrders).to.deep.equal([ - testOrderWithFeeInMakerAsset2, - testOrderWithFeeInMakerAsset1, - testOrderWithFeeInMakerAsset3, - ]); - }); - it('correctly sorts by fee adjusted rate (mixed orders)', async () => { - const orders = [testOrderWithFeeInMakerAsset1, testOrderWithFeeInTakerAsset2, testOrder3]; - const sortedOrders = sortingUtils.sortOrders(orders); - expect(sortedOrders).to.deep.equal([ - testOrderWithFeeInTakerAsset2, - testOrderWithFeeInMakerAsset1, - testOrder3, - ]); - }); - }); -}); diff --git a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts deleted file mode 100644 index a4022a5407..0000000000 --- a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { WETH9Contract } from '@0x/contract-wrappers'; -import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; -import { migrateOnceAsync } from '@0x/migrations'; -import { assetDataUtils } from '@0x/order-utils'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { SwapQuote, SwapQuoteConsumer } from '../src'; -import { constants } from '../src/constants'; -import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -const TESTRPC_CHAIN_ID = 1337; -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); - -const PARTIAL_PRUNED_SIGNED_ORDERS: Array> = [ - { - takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - }, -]; - -const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array> = [ - { - takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI), - }, -]; - -describe('swapQuoteConsumerUtils', () => { - let wethContract: WETH9Contract; - let userAddresses: string[]; - let makerAddress: string; - let takerAddress: string; - let makerTokenAddress: string; - let takerTokenAddress: string; - let makerAssetData: string; - let takerAssetData: string; - let wethAssetData: string; - let contractAddresses: ContractAddresses; - let swapQuoteConsumer: SwapQuoteConsumer; - let orderFactory: OrderFactory; - let forwarderOrderFactory: OrderFactory; - - const chainId = TESTRPC_CHAIN_ID; - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - await blockchainLifecycle.startAsync(); - userAddresses = await web3Wrapper.getAvailableAddressesAsync(); - wethContract = new WETH9Contract(contractAddresses.etherToken, provider); - [takerAddress, makerAddress] = userAddresses; - [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); - [makerAssetData, takerAssetData, wethAssetData] = [ - assetDataUtils.encodeERC20AssetData(makerTokenAddress), - assetDataUtils.encodeERC20AssetData(takerTokenAddress), - assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken), - ]; - - const defaultOrderParams = { - ...devConstants.STATIC_ORDER_PARAMS, - makerAddress, - takerAddress: constants.NULL_ADDRESS, - makerAssetData, - takerAssetData, - makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - feeRecipientAddress: constants.NULL_ADDRESS, - exchangeAddress: contractAddresses.exchange, - chainId, - }; - const defaultForwarderOrderParams = { - ...defaultOrderParams, - ...{ - takerAssetData: wethAssetData, - }, - }; - const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams); - - swapQuoteConsumer = new SwapQuoteConsumer(provider, { - chainId, - contractAddresses, - }); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('getConsumerTypeForSwapQuoteAsync', () => { - let forwarderOrders: SignedOrderWithFillableAmounts[]; - let exchangeOrders: SignedOrderWithFillableAmounts[]; - let largeForwarderOrders: SignedOrderWithFillableAmounts[]; - let forwarderSwapQuote: SwapQuote; - let exchangeSwapQuote: SwapQuote; - let largeForwarderSwapQuote: SwapQuote; - - beforeEach(async () => { - exchangeOrders = []; - for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) { - const order = await orderFactory.newSignedOrderAsync(partialOrder); - const prunedOrder = { - ...order, - ...partialOrder, - }; - exchangeOrders.push(prunedOrder as SignedOrderWithFillableAmounts); - } - - forwarderOrders = []; - for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) { - const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder); - const prunedOrder = { - ...order, - ...partialOrder, - }; - forwarderOrders.push(prunedOrder as SignedOrderWithFillableAmounts); - } - - largeForwarderOrders = []; - for (const partialOrder of PARTIAL_LARGE_PRUNED_SIGNED_ORDERS) { - const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder); - const prunedOrder = { - ...order, - ...partialOrder, - }; - largeForwarderOrders.push(prunedOrder as SignedOrderWithFillableAmounts); - } - - forwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - wethAssetData, - forwarderOrders, - MarketOperation.Sell, - GAS_PRICE, - ); - - largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - wethAssetData, - largeForwarderOrders, - MarketOperation.Sell, - GAS_PRICE, - ); - - exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - takerAssetData, - exchangeOrders, - MarketOperation.Sell, - GAS_PRICE, - ); - }); - - it('should return exchange consumer if takerAsset is not wEth', async () => { - const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync( - exchangeSwapQuote, - { takerAddress }, - ); - expect(extensionContractType).to.equal(ExtensionContractType.None); - }); - it('should return forwarder consumer if takerAsset is wEth and have enough eth balance', async () => { - const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync( - forwarderSwapQuote, - { takerAddress }, - ); - expect(extensionContractType).to.equal(ExtensionContractType.Forwarder); - }); - it('should return exchange consumer if takerAsset is wEth and taker has enough weth', async () => { - const etherInWei = new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI); - await wethContract.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress }); - const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync( - forwarderSwapQuote, - { takerAddress }, - ); - expect(extensionContractType).to.equal(ExtensionContractType.None); - }); - it('should return forwarder consumer if takerAsset is wEth and takerAddress has no available balance in either weth or eth (defaulting behavior)', async () => { - const etherInWei = new BigNumber(50).multipliedBy(ONE_ETH_IN_WEI); - await wethContract.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress }); - const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync( - largeForwarderSwapQuote, - { takerAddress }, - ); - expect(extensionContractType).to.equal(ExtensionContractType.Forwarder); - }); - }); -}); diff --git a/packages/asset-swapper/test/swap_quoter_test.ts b/packages/asset-swapper/test/swap_quoter_test.ts deleted file mode 100644 index df5f12d498..0000000000 --- a/packages/asset-swapper/test/swap_quoter_test.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { Orderbook } from '@0x/orderbook'; -import { Web3ProviderEngine } from '@0x/subproviders'; -import { AssetPairsItem, SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; -import * as TypeMoq from 'typemoq'; - -import { SwapQuoter } from '../src'; -import { constants } from '../src/constants'; -import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../src/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { mockAvailableAssetDatas, mockedSwapQuoterWithFillableAmounts, orderbookMock } from './utils/mocks'; -import { testOrderFactory } from './utils/test_order_factory'; -import { baseUnitAmount } from './utils/utils'; - -chaiSetup.configure(); -const expect = chai.expect; - -const FAKE_SRA_URL = 'https://fakeurl.com'; -const FAKE_TAKER_ASSET_DATA = '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48'; -const FAKE_MAKER_ASSET_DATA = '0xf47261b00000000000000000000000009f5B0C7e1623793bF0620569b9749e79DF6D0bC5'; -const TOKEN_DECIMALS = 18; -const DAI_ASSET_DATA = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359"'; -const WETH_ASSET_DATA = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; -const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS; -const ZERO = new BigNumber(0); - -const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string): AssetPairsItem[] => { - const defaultAssetPairItem = { - minAmount: ZERO, - maxAmount: ZERO, - precision: TOKEN_DECIMALS, - }; - return [ - { - assetDataA: { - ...defaultAssetPairItem, - assetData: makerAssetData, - }, - assetDataB: { - ...defaultAssetPairItem, - assetData: takerAssetData, - }, - }, - { - assetDataA: { - ...defaultAssetPairItem, - assetData: takerAssetData, - }, - assetDataB: { - ...defaultAssetPairItem, - assetData: makerAssetData, - }, - }, - ]; -}; - -const expectLiquidityResult = async ( - web3Provider: Web3ProviderEngine, - orderbook: Orderbook, - orders: SignedOrderWithFillableAmounts[], - expectedLiquidityResult: LiquidityForTakerMakerAssetDataPair, -) => { - const mockedSwapQuoter = mockedSwapQuoterWithFillableAmounts( - web3Provider, - orderbook, - FAKE_MAKER_ASSET_DATA, - WETH_ASSET_DATA, - orders, - ); - const liquidityResult = await mockedSwapQuoter.object.getLiquidityForMakerTakerAssetDataPairAsync( - FAKE_MAKER_ASSET_DATA, - WETH_ASSET_DATA, - ); - expect(liquidityResult).to.deep.equal(expectedLiquidityResult); -}; - -// tslint:disable:custom-no-magic-numbers -describe('SwapQuoter', () => { - describe('getLiquidityForMakerTakerAssetDataPairAsync', () => { - const mockWeb3Provider = TypeMoq.Mock.ofType(Web3ProviderEngine); - const mockOrderbook = orderbookMock(); - - beforeEach(() => { - mockWeb3Provider.reset(); - mockOrderbook.reset(); - }); - - afterEach(() => { - mockWeb3Provider.verifyAll(); - mockOrderbook.verifyAll(); - }); - - describe('validation', () => { - it('should ensure takerAssetData is a string', async () => { - const swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl( - mockWeb3Provider.object, - FAKE_SRA_URL, - ); - - expect( - swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(FAKE_MAKER_ASSET_DATA, false as any), - ).to.be.rejectedWith('Expected takerAssetData to be of type string, encountered: false'); - }); - it('should ensure makerAssetData is a string', async () => { - const swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl( - mockWeb3Provider.object, - FAKE_SRA_URL, - ); - - expect( - swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(false as any, FAKE_TAKER_ASSET_DATA), - ).to.be.rejectedWith('Expected makerAssetData to be of type string, encountered: false'); - }); - }); - - describe('asset pair not supported', () => { - it('should return 0s when no asset pair are supported', async () => { - mockAvailableAssetDatas(mockOrderbook, []); - - const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderbook.object); - const liquidityResult = await swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync( - FAKE_MAKER_ASSET_DATA, - FAKE_TAKER_ASSET_DATA, - ); - expect(liquidityResult).to.deep.equal({ - makerAssetAvailableInBaseUnits: new BigNumber(0), - takerAssetAvailableInBaseUnits: new BigNumber(0), - }); - }); - - it('should return 0s when only other asset pair supported', async () => { - mockAvailableAssetDatas(mockOrderbook, assetsToAssetPairItems(FAKE_MAKER_ASSET_DATA, DAI_ASSET_DATA)); - - const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderbook.object); - const liquidityResult = await swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync( - FAKE_MAKER_ASSET_DATA, - FAKE_TAKER_ASSET_DATA, - ); - expect(liquidityResult).to.deep.equal({ - makerAssetAvailableInBaseUnits: new BigNumber(0), - takerAssetAvailableInBaseUnits: new BigNumber(0), - }); - }); - }); - - describe('assetData is supported', () => { - // orders - const sellTenTokensFor10Weth: SignedOrder = testOrderFactory.generateTestSignedOrder({ - makerAssetAmount: baseUnitAmount(10), - takerAssetAmount: baseUnitAmount(10, WETH_DECIMALS), - chainId: 42, - }); - - beforeEach(() => { - mockAvailableAssetDatas(mockOrderbook, assetsToAssetPairItems(WETH_ASSET_DATA, FAKE_MAKER_ASSET_DATA)); - }); - - it('should return 0s when no orders available', async () => { - const orders: SignedOrderWithFillableAmounts[] = []; - const expectedResult = { - makerAssetAvailableInBaseUnits: new BigNumber(0), - takerAssetAvailableInBaseUnits: new BigNumber(0), - }; - await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult); - }); - - it('should return correct computed value when orders provided with full fillableAmounts', async () => { - const orders: SignedOrderWithFillableAmounts[] = [ - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: sellTenTokensFor10Weth.makerAssetAmount, - fillableTakerAssetAmount: sellTenTokensFor10Weth.takerAssetAmount, - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: sellTenTokensFor10Weth.makerAssetAmount, - fillableTakerAssetAmount: sellTenTokensFor10Weth.takerAssetAmount, - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - ]; - const expectedMakerAssetAvailable = orders[0].makerAssetAmount.plus(orders[1].makerAssetAmount); - const expectedTakerAssetAvailable = orders[0].takerAssetAmount.plus(orders[1].takerAssetAmount); - - const expectedResult = { - makerAssetAvailableInBaseUnits: expectedMakerAssetAvailable, - takerAssetAvailableInBaseUnits: expectedTakerAssetAvailable, - }; - - await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult); - }); - - it('should return correct computed value with one partial fillableAmounts', async () => { - const orders: SignedOrderWithFillableAmounts[] = [ - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: baseUnitAmount(1), - fillableTakerAssetAmount: baseUnitAmount(0.5, WETH_DECIMALS), - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - ]; - - const expectedResult = { - makerAssetAvailableInBaseUnits: baseUnitAmount(1), - takerAssetAvailableInBaseUnits: baseUnitAmount(0.5, WETH_DECIMALS), - }; - - await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult); - }); - - it('should return correct computed value with multiple orders and fillable amounts', async () => { - const orders: SignedOrderWithFillableAmounts[] = [ - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: baseUnitAmount(1), - fillableTakerAssetAmount: baseUnitAmount(0.5, WETH_DECIMALS), - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: baseUnitAmount(3), - fillableTakerAssetAmount: baseUnitAmount(3, WETH_DECIMALS), - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - ]; - - const expectedResult = { - makerAssetAvailableInBaseUnits: baseUnitAmount(4), - takerAssetAvailableInBaseUnits: baseUnitAmount(3.5, WETH_DECIMALS), - }; - - await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult); - }); - - it('should return 0s when no amounts fillable', async () => { - const orders: SignedOrderWithFillableAmounts[] = [ - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: constants.ZERO_AMOUNT, - fillableTakerAssetAmount: constants.ZERO_AMOUNT, - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - { - ...sellTenTokensFor10Weth, - ...{ - fillableMakerAssetAmount: constants.ZERO_AMOUNT, - fillableTakerAssetAmount: constants.ZERO_AMOUNT, - fillableTakerFeeAmount: constants.ZERO_AMOUNT, - }, - }, - ]; - - const expectedResult = { - makerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT, - takerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT, - }; - - await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult); - }); - }); - }); -}); diff --git a/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts b/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts index fabfdbd986..a5b12a2dcd 100644 --- a/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts +++ b/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts @@ -1,8 +1,11 @@ -import { BalancerPool, BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils'; +import { Pool } from '@balancer-labs/sor/dist/types'; + +import { BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils'; export interface Handlers { - getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise; - _fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise; + getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise; + _fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise; + _loadTopPoolsAsync: () => Promise; } export class MockBalancerPoolsCache extends BalancerPoolsCache { @@ -10,15 +13,21 @@ export class MockBalancerPoolsCache extends BalancerPoolsCache { super(); } - public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise { return this.handlers.getPoolsForPairAsync ? this.handlers.getPoolsForPairAsync(takerToken, makerToken) : super.getPoolsForPairAsync(takerToken, makerToken); } - protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { return this.handlers._fetchPoolsForPairAsync ? this.handlers._fetchPoolsForPairAsync(takerToken, makerToken) : super._fetchPoolsForPairAsync(takerToken, makerToken); } + + protected async _loadTopPoolsAsync(): Promise { + if (this.handlers && this.handlers._loadTopPoolsAsync) { + return this.handlers._loadTopPoolsAsync(); + } + } } diff --git a/packages/asset-swapper/test/utils/mock_sampler_contract.ts b/packages/asset-swapper/test/utils/mock_sampler_contract.ts index 03aae79f0e..a5c03238d5 100644 --- a/packages/asset-swapper/test/utils/mock_sampler_contract.ts +++ b/packages/asset-swapper/test/utils/mock_sampler_contract.ts @@ -1,6 +1,6 @@ import { ContractTxFunctionObj } from '@0x/base-contract'; import { constants } from '@0x/contracts-test-utils'; -import { Order } from '@0x/types'; +import { LimitOrderFields, Signature } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; import { SamplerCallResult } from '../../src/types'; @@ -8,8 +8,8 @@ import { ERC20BridgeSamplerContract } from '../../src/wrappers'; export type GetOrderFillableAssetAmountResult = BigNumber[]; export type GetOrderFillableAssetAmountHandler = ( - orders: Order[], - signatures: string[], + orders: LimitOrderFields[], + signatures: Signature[], devUtilsAddress: string, ) => GetOrderFillableAssetAmountResult; @@ -36,6 +36,7 @@ export type SampleBuysKyberHandler = ( makerToken: string, makerTokenAmounts: BigNumber[], ) => [string, SampleResults]; +export type SampleUniswapV2Handler = (router: string, path: string[], assetAmounts: BigNumber[]) => SampleResults; export type SampleBuysMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults; export type SampleSellsLPHandler = ( providerAddress: string, @@ -52,16 +53,16 @@ const DUMMY_PROVIDER = { }; interface Handlers { - getOrderFillableMakerAssetAmounts: GetOrderFillableAssetAmountHandler; - getOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler; + getLimitOrderFillableMakerAssetAmounts: GetOrderFillableAssetAmountHandler; + getLimitOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler; sampleSellsFromKyberNetwork: SampleSellsKyberHandler; sampleSellsFromLiquidityProvider: SampleSellsLPHandler; sampleSellsFromEth2Dai: SampleSellsHandler; sampleSellsFromUniswap: SampleSellsHandler; - sampleSellsFromUniswapV2: SampleSellsMultihopHandler; + sampleSellsFromUniswapV2: SampleUniswapV2Handler; sampleBuysFromEth2Dai: SampleBuysHandler; sampleBuysFromUniswap: SampleBuysHandler; - sampleBuysFromUniswapV2: SampleBuysMultihopHandler; + sampleBuysFromUniswapV2: SampleUniswapV2Handler; sampleBuysFromLiquidityProvider: SampleSellsLPHandler; } @@ -83,26 +84,26 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { }; } - public getOrderFillableMakerAssetAmounts( - orders: Order[], - signatures: string[], + public getLimitOrderFillableMakerAssetAmounts( + orders: LimitOrderFields[], + signatures: Signature[], ): ContractTxFunctionObj { return this._wrapCall( - super.getOrderFillableMakerAssetAmounts, - this._handlers.getOrderFillableMakerAssetAmounts, + super.getLimitOrderFillableMakerAssetAmounts, + this._handlers.getLimitOrderFillableMakerAssetAmounts, orders, signatures, constants.NULL_ADDRESS, ); } - public getOrderFillableTakerAssetAmounts( - orders: Order[], - signatures: string[], + public getLimitOrderFillableTakerAssetAmounts( + orders: LimitOrderFields[], + signatures: Signature[], ): ContractTxFunctionObj { return this._wrapCall( - super.getOrderFillableTakerAssetAmounts, - this._handlers.getOrderFillableTakerAssetAmounts, + super.getLimitOrderFillableTakerAssetAmounts, + this._handlers.getLimitOrderFillableTakerAssetAmounts, orders, signatures, constants.NULL_ADDRESS, @@ -154,12 +155,14 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { } public sampleSellsFromUniswapV2( + router: string, path: string[], takerAssetAmounts: BigNumber[], ): ContractTxFunctionObj { return this._wrapCall( super.sampleSellsFromUniswapV2, this._handlers.sampleSellsFromUniswapV2, + router, path, takerAssetAmounts, ); @@ -209,10 +212,15 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { ); } - public sampleBuysFromUniswapV2(path: string[], makerAssetAmounts: BigNumber[]): ContractTxFunctionObj { + public sampleBuysFromUniswapV2( + router: string, + path: string[], + makerAssetAmounts: BigNumber[], + ): ContractTxFunctionObj { return this._wrapCall( super.sampleBuysFromUniswapV2, this._handlers.sampleBuysFromUniswapV2, + router, path, makerAssetAmounts, ); diff --git a/packages/asset-swapper/test/utils/mocks.ts b/packages/asset-swapper/test/utils/mocks.ts deleted file mode 100644 index 25161cde33..0000000000 --- a/packages/asset-swapper/test/utils/mocks.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { constants as devConstants } from '@0x/contracts-test-utils'; -import { AcceptedRejectedOrders, Orderbook } from '@0x/orderbook'; -import { Web3ProviderEngine } from '@0x/subproviders'; -import { APIOrder, AssetPairsItem, SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as TypeMoq from 'typemoq'; - -import { SwapQuoter } from '../../src/swap_quoter'; -import { SignedOrderWithFillableAmounts } from '../../src/types'; -import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils'; - -// tslint:disable: max-classes-per-file - -class OrderbookClass extends Orderbook { - // tslint:disable-next-line:prefer-function-over-method - public async getOrdersAsync(_makerAssetData: string, _takerAssetData: string): Promise { - return []; - } - // tslint:disable-next-line:prefer-function-over-method - public async getAvailableAssetDatasAsync(): Promise { - return []; - } - // tslint:disable-next-line:prefer-function-over-method - public async addOrdersAsync(_orders: SignedOrder[]): Promise { - return { accepted: [], rejected: [] }; - } -} -export const orderbookMock = () => { - return TypeMoq.Mock.ofType(OrderbookClass, TypeMoq.MockBehavior.Strict); -}; - -export const mockAvailableAssetDatas = ( - mockOrderbook: TypeMoq.IMock, - availableAssetDatas: AssetPairsItem[], -) => { - mockOrderbook - .setup(async op => op.getAvailableAssetDatasAsync()) - .returns(async () => availableAssetDatas) - .verifiable(TypeMoq.Times.once()); - mockOrderbook - .setup(o => (o as any)._orderProvider) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeast(0)); - mockOrderbook - .setup(o => (o as any)._orderStore) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeast(0)); -}; - -const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orderbook): TypeMoq.IMock => { - const rawSwapQuoter = new SwapQuoter(provider, orderbook); - const mockedSwapQuoter = TypeMoq.Mock.ofInstance(rawSwapQuoter, TypeMoq.MockBehavior.Loose, false); - mockedSwapQuoter.callBase = true; - return mockedSwapQuoter; -}; - -class ProtocolFeeUtilsClass { - public static getInstance(..._args: any[]): any { - return { - getGasPriceEstimationOrThrowAsync: async () => - Promise.resolve(new BigNumber(devConstants.DEFAULT_GAS_PRICE)), - }; - } - // tslint:disable-next-line:prefer-function-over-method - public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise { - return new BigNumber(devConstants.DEFAULT_GAS_PRICE); - } -} - -export const protocolFeeUtilsMock = (): TypeMoq.IMock => { - const mockProtocolFeeUtils = TypeMoq.Mock.ofType(ProtocolFeeUtilsClass, TypeMoq.MockBehavior.Loose); - mockProtocolFeeUtils.callBase = true; - return mockProtocolFeeUtils as any; -}; - -const mockGetSignedOrdersWithFillableAmountsAsyncAsync = ( - mockedSwapQuoter: TypeMoq.IMock, - makerAssetData: string, - takerAssetData: string, - signedOrders: SignedOrderWithFillableAmounts[], -): void => { - mockedSwapQuoter - .setup(async a => a.getSignedOrdersWithFillableAmountsAsync(makerAssetData, takerAssetData)) - .returns(async () => signedOrders) - .verifiable(TypeMoq.Times.once()); -}; - -export const mockedSwapQuoterWithFillableAmounts = ( - provider: Web3ProviderEngine, - orderbook: Orderbook, - makerAssetData: string, - takerAssetData: string, - signedOrders: SignedOrderWithFillableAmounts[], -): TypeMoq.IMock => { - const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderbook); - mockGetSignedOrdersWithFillableAmountsAsyncAsync(mockedAssetQuoter, makerAssetData, takerAssetData, signedOrders); - return mockedAssetQuoter; -}; diff --git a/packages/asset-swapper/test/utils/swap_quote.ts b/packages/asset-swapper/test/utils/swap_quote.ts index 32da56f6bc..6e0d174d12 100644 --- a/packages/asset-swapper/test/utils/swap_quote.ts +++ b/packages/asset-swapper/test/utils/swap_quote.ts @@ -1,27 +1,27 @@ import { BigNumber } from '@0x/utils'; -import { ERC20BridgeSource } from '../../src'; +import { ERC20BridgeSource, OptimizedMarketOrder } from '../../src'; import { constants } from '../../src/constants'; -import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types'; +import { MarketOperation, SwapQuote, SwapQuoteBase } from '../../src/types'; /** * Creates a swap quote given orders. */ export async function getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData: string, - takerAssetData: string, - orders: SignedOrderWithFillableAmounts[], + makerToken: string, + takerToken: string, + orders: OptimizedMarketOrder[], operation: MarketOperation, gasPrice: BigNumber, ): Promise { - const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]); - const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]); + const makerAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAmount)]); + const takerAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAmount)]); const protocolFeePerOrder = constants.PROTOCOL_FEE_MULTIPLIER.times(gasPrice); const quoteInfo = { - makerAssetAmount: makerAssetFillAmount, - feeTakerAssetAmount: constants.ZERO_AMOUNT, - takerAssetAmount: totalTakerAssetAmount, - totalTakerAssetAmount, + makerAmount, + feeTakerTokenAmount: constants.ZERO_AMOUNT, + takerAmount, + totalTakerAmount: takerAmount, protocolFeeInWeiAmount: protocolFeePerOrder.times(orders.length), gas: 200e3, }; @@ -30,36 +30,32 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync( [ERC20BridgeSource.Native]: new BigNumber(1), }; - const quoteBase = { - makerAssetData, - takerAssetData, + const quoteBase: SwapQuoteBase = { + makerToken, + takerToken, orders: orders.map(order => ({ ...order, fills: [] })), gasPrice, bestCaseQuoteInfo: quoteInfo, worstCaseQuoteInfo: quoteInfo, - unoptimizedQuoteInfo: quoteInfo, - unoptimizedOrders: orders.map(order => ({ ...order, fills: [] })), sourceBreakdown: breakdown, isTwoHop: false, - takerAssetToEthRate: constants.ZERO_AMOUNT, - makerAssetToEthRate: constants.ZERO_AMOUNT, + takerTokenToEthRate: constants.ZERO_AMOUNT, + makerTokenToEthRate: constants.ZERO_AMOUNT, + makerTokenDecimals: 18, + takerTokenDecimals: 18, }; if (operation === MarketOperation.Buy) { return { ...quoteBase, type: MarketOperation.Buy, - makerAssetFillAmount, - makerTokenDecimals: 18, - takerTokenDecimals: 18, + makerTokenFillAmount: makerAmount, }; } else { return { ...quoteBase, type: MarketOperation.Sell, - takerAssetFillAmount: totalTakerAssetAmount, - makerTokenDecimals: 18, - takerTokenDecimals: 18, + takerTokenFillAmount: takerAmount, }; } } diff --git a/packages/asset-swapper/test/utils/test_helpers.ts b/packages/asset-swapper/test/utils/test_helpers.ts index efbb287bdf..6bd389ca9b 100644 --- a/packages/asset-swapper/test/utils/test_helpers.ts +++ b/packages/asset-swapper/test/utils/test_helpers.ts @@ -1,6 +1,14 @@ import { BigNumber } from '@0x/utils'; +import axios, { AxiosInstance } from 'axios'; +import AxiosMockAdapter from 'axios-mock-adapter'; import { InsufficientAssetLiquidityError } from '../../src/errors'; +import { MockedRfqtQuoteResponse } from '../../src/types'; + +export enum RfqtQuoteEndpoint { + Indicative = 'price', + Firm = 'quote', +} export const testHelpers = { expectInsufficientLiquidityErrorAsync: async ( @@ -23,4 +31,31 @@ export const testHelpers = { expect(wasErrorThrown).to.be.true(); }, + /** + * A helper utility for testing which mocks out + * requests to RFQ-t providers + */ + withMockedRfqtQuotes: async ( + mockedResponses: MockedRfqtQuoteResponse[], + quoteType: RfqtQuoteEndpoint, + afterResponseCallback: () => Promise, + axiosClient: AxiosInstance = axios, + ): Promise => { + const mockedAxios = new AxiosMockAdapter(axiosClient); + try { + // Mock out RFQT responses + for (const mockedResponse of mockedResponses) { + const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; + const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; + mockedAxios + .onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders) + .replyOnce(responseCode, responseData); + } + // Perform the callback function, e.g. a test validation + await afterResponseCallback(); + } finally { + // Ensure we always restore axios afterwards + mockedAxios.restore(); + } + }, }; diff --git a/packages/asset-swapper/test/utils/test_order_factory.ts b/packages/asset-swapper/test/utils/test_order_factory.ts deleted file mode 100644 index 2c577de88d..0000000000 --- a/packages/asset-swapper/test/utils/test_order_factory.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { orderFactory } from '@0x/order-utils/lib/src/order_factory'; -import { Order, SignedOrder } from '@0x/types'; -import * as _ from 'lodash'; - -import { constants } from '../../src/constants'; -import { SignedOrderWithFillableAmounts } from '../../src/types'; - -const CHAIN_ID = 1337; -const BASE_TEST_ORDER: Order = orderFactory.createOrder( - constants.NULL_ADDRESS, - constants.ZERO_AMOUNT, - constants.NULL_ERC20_ASSET_DATA, - constants.ZERO_AMOUNT, - constants.NULL_ERC20_ASSET_DATA, - constants.NULL_ADDRESS, - CHAIN_ID, -); - -const BASE_TEST_SIGNED_ORDER: SignedOrder = { - ...BASE_TEST_ORDER, - signature: constants.NULL_BYTES, -}; - -const BASE_TEST_PRUNED_SIGNED_ORDER: SignedOrderWithFillableAmounts = { - ...BASE_TEST_SIGNED_ORDER, - fillableMakerAssetAmount: constants.ZERO_AMOUNT, - fillableTakerAssetAmount: constants.ZERO_AMOUNT, - fillableTakerFeeAmount: constants.ZERO_AMOUNT, -}; - -export const testOrderFactory = { - generateTestSignedOrder(partialOrder: Partial): SignedOrder { - return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder); - }, - generateIdenticalTestSignedOrders(partialOrder: Partial, numOrders: number): SignedOrder[] { - const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_SIGNED_ORDER); - return _.map(baseTestOrders, order => transformObject(order, partialOrder)); - }, - generateTestSignedOrders(partialOrders: Array>): SignedOrder[] { - return _.map(partialOrders, partialOrder => transformObject(BASE_TEST_SIGNED_ORDER, partialOrder)); - }, - generateTestSignedOrderWithFillableAmounts( - partialOrder: Partial, - ): SignedOrderWithFillableAmounts { - return transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder); - }, - generateIdenticalTestSignedOrdersWithFillableAmounts( - partialOrder: Partial, - numOrders: number, - ): SignedOrderWithFillableAmounts[] { - const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_PRUNED_SIGNED_ORDER); - return _.map( - baseTestOrders, - (baseOrder): SignedOrderWithFillableAmounts => transformObject(baseOrder, partialOrder), - ); - }, - generateTestSignedOrdersWithFillableAmounts( - partialOrders: Array>, - ): SignedOrderWithFillableAmounts[] { - return _.map( - partialOrders, - (partialOrder): SignedOrderWithFillableAmounts => - transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder), - ); - }, -}; - -function transformObject(input: T, transformation: Partial): T { - const copy = _.cloneDeep(input); - return _.assign(copy, transformation); -} diff --git a/packages/asset-swapper/test/utils/test_orders.ts b/packages/asset-swapper/test/utils/test_orders.ts deleted file mode 100644 index 5a2581c231..0000000000 --- a/packages/asset-swapper/test/utils/test_orders.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { SignedOrderWithFillableAmounts } from '../../src/types'; - -import { testOrderFactory } from './test_order_factory'; -import { baseUnitAmount } from './utils'; - -// tslint:disable:custom-no-magic-numbers - -const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b02222222222222222222222222222222222222222222222222222222222222222'; -const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b01111111111111111111111111111111111111111111111111111111111111111'; - -const PARTIAL_ORDER: Partial = { - takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA, -}; - -const PARTIAL_ORDER_FEE_IN_TAKER_ASSET: Partial = { - ...{ - takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA, - }, - ...PARTIAL_ORDER, -}; - -const PARTIAL_ORDER_FEE_IN_MAKER_ASSET: Partial = { - ...{ - takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA, - }, - ...PARTIAL_ORDER, -}; - -const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS: Array> = [ - { - ...{ - takerAssetAmount: baseUnitAmount(1), - makerAssetAmount: baseUnitAmount(6), - fillableTakerAssetAmount: baseUnitAmount(1), - fillableMakerAssetAmount: baseUnitAmount(6), - }, - ...PARTIAL_ORDER, - }, - { - ...{ - takerAssetAmount: baseUnitAmount(10), - makerAssetAmount: baseUnitAmount(4), - fillableTakerAssetAmount: baseUnitAmount(5), - fillableMakerAssetAmount: baseUnitAmount(2), - }, - ...PARTIAL_ORDER, - }, - { - ...{ - takerAssetAmount: baseUnitAmount(6), - makerAssetAmount: baseUnitAmount(6), - fillableTakerAssetAmount: baseUnitAmount(3), - fillableMakerAssetAmount: baseUnitAmount(3), - }, - ...PARTIAL_ORDER, - }, -]; - -const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET: Array> = [ - { - ...{ - takerAssetAmount: baseUnitAmount(1), - makerAssetAmount: baseUnitAmount(6), - takerFee: baseUnitAmount(3), - fillableTakerAssetAmount: baseUnitAmount(1), - fillableMakerAssetAmount: baseUnitAmount(6), - fillableTakerFeeAmount: baseUnitAmount(3), - }, - ...PARTIAL_ORDER_FEE_IN_TAKER_ASSET, - }, - { - ...{ - takerAssetAmount: baseUnitAmount(10), - makerAssetAmount: baseUnitAmount(4), - takerFee: baseUnitAmount(2), - fillableTakerAssetAmount: baseUnitAmount(5), - fillableMakerAssetAmount: baseUnitAmount(2), - fillableTakerFeeAmount: baseUnitAmount(1), - }, - ...PARTIAL_ORDER_FEE_IN_TAKER_ASSET, - }, - { - ...{ - takerAssetAmount: baseUnitAmount(6), - makerAssetAmount: baseUnitAmount(6), - takerFee: baseUnitAmount(4), - fillableTakerAssetAmount: baseUnitAmount(3), - fillableMakerAssetAmount: baseUnitAmount(3), - fillableTakerFeeAmount: baseUnitAmount(2), - }, - ...PARTIAL_ORDER_FEE_IN_TAKER_ASSET, - }, -]; - -const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET: Array> = [ - { - ...{ - takerAssetAmount: baseUnitAmount(5), - makerAssetAmount: baseUnitAmount(2), - takerFee: baseUnitAmount(1), - fillableTakerAssetAmount: baseUnitAmount(5), - fillableMakerAssetAmount: baseUnitAmount(2), - fillableTakerFeeAmount: baseUnitAmount(1), - }, - ...PARTIAL_ORDER_FEE_IN_MAKER_ASSET, - }, - { - ...{ - takerAssetAmount: baseUnitAmount(2), - makerAssetAmount: baseUnitAmount(12), - takerFee: baseUnitAmount(6), - fillableTakerAssetAmount: baseUnitAmount(1), - fillableMakerAssetAmount: baseUnitAmount(6), - fillableTakerFeeAmount: baseUnitAmount(3), - }, - ...PARTIAL_ORDER_FEE_IN_MAKER_ASSET, - }, - { - ...{ - takerAssetAmount: baseUnitAmount(3), - makerAssetAmount: baseUnitAmount(3), - takerFee: baseUnitAmount(2), - fillableTakerAssetAmount: baseUnitAmount(3), - fillableMakerAssetAmount: baseUnitAmount(3), - fillableTakerFeeAmount: baseUnitAmount(2), - }, - ...PARTIAL_ORDER_FEE_IN_MAKER_ASSET, - }, -]; - -const SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS = testOrderFactory.generateTestSignedOrdersWithFillableAmounts( - PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, -); -const SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET = testOrderFactory.generateTestSignedOrdersWithFillableAmounts( - PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, -); -const SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET = testOrderFactory.generateTestSignedOrdersWithFillableAmounts( - PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, -); - -export const testOrders = { - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, -}; diff --git a/packages/asset-swapper/test/utils/utils.ts b/packages/asset-swapper/test/utils/utils.ts index 1e01d59f0e..cc0e14c825 100644 --- a/packages/asset-swapper/test/utils/utils.ts +++ b/packages/asset-swapper/test/utils/utils.ts @@ -1,4 +1,6 @@ -import { BigNumber } from '@0x/utils'; +import { getRandomInteger } from '@0x/contracts-test-utils'; +import { Signature, SignatureType } from '@0x/protocol-utils'; +import { BigNumber, generatePseudoRandom256BitNumber, hexUtils, Numberish } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; const TOKEN_DECIMALS = 18; @@ -7,3 +9,22 @@ const TOKEN_DECIMALS = 18; export const baseUnitAmount = (unitAmount: number, decimals = TOKEN_DECIMALS): BigNumber => { return Web3Wrapper.toBaseUnitAmount(new BigNumber(unitAmount), decimals); }; + +// tslint:disable:completed-docs +export function generatePseudoRandomSalt(): BigNumber { + const salt = generatePseudoRandom256BitNumber(); + return salt; +} + +export function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber { + return getRandomInteger(1, maxAmount); +} + +export function getRandomSignature(): Signature { + return { + v: 1, + r: hexUtils.random(32), + s: hexUtils.random(32), + signatureType: SignatureType.Invalid, + }; +} diff --git a/packages/asset-swapper/test/utils_test.ts b/packages/asset-swapper/test/utils_test.ts deleted file mode 100644 index 2ee2cb13bc..0000000000 --- a/packages/asset-swapper/test/utils_test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { tokenUtils } from '@0x/dev-utils'; -import { assetDataUtils } from '@0x/order-utils'; -import { BigNumber, NULL_ADDRESS, NULL_BYTES } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { isAssetDataEquivalent } from '../src/utils/utils'; - -import { chaiSetup } from './utils/chai_setup'; - -chaiSetup.configure(); -const expect = chai.expect; - -describe('utils', () => { - describe('isAssetDataEquivalent', () => { - describe('ERC20', () => { - const [tokenA, tokenB] = tokenUtils.getDummyERC20TokenAddresses(); - it('should succeed ERC20 to be ERC20Bridge', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC20AssetData(tokenA), - assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES), - ); - expect(isEquivalent).to.be.true(); - }); - it('should succeed ERC20Bridge to be ERC20', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES), - assetDataUtils.encodeERC20AssetData(tokenA), - ); - expect(isEquivalent).to.be.true(); - }); - it('should succeed ERC20 to be ERC20', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC20AssetData(tokenA), - assetDataUtils.encodeERC20AssetData(tokenA), - ); - expect(isEquivalent).to.be.true(); - }); - it('should fail if ERC20Bridge is not the same ERC20 token', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC20AssetData(tokenA), - assetDataUtils.encodeERC20BridgeAssetData(tokenB, NULL_ADDRESS, NULL_BYTES), - ); - expect(isEquivalent).to.be.false(); - }); - it('should fail if ERC20 is not the same ERC20 token', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC20AssetData(tokenA), - assetDataUtils.encodeERC20AssetData(tokenB), - ); - expect(isEquivalent).to.be.false(); - }); - }); - describe('ERC721', () => { - const [tokenA, tokenB] = tokenUtils.getDummyERC20TokenAddresses(); - const tokenIdA = new BigNumber(1); - const tokenIdB = new BigNumber(2); - it('should succeed if ERC721 the same ERC721 token and id', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), - assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), - ); - expect(isEquivalent).to.be.true(); - }); - it('should fail if ERC721 is not the same ERC721 token', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), - assetDataUtils.encodeERC721AssetData(tokenB, tokenIdA), - ); - expect(isEquivalent).to.be.false(); - }); - it('should fail if ERC721 is not the same ERC721 id', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), - assetDataUtils.encodeERC721AssetData(tokenA, tokenIdB), - ); - expect(isEquivalent).to.be.false(); - }); - it('should fail if ERC721 is compared with ERC20', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), - assetDataUtils.encodeERC20AssetData(tokenA), - ); - expect(isEquivalent).to.be.false(); - }); - }); - describe('ERC1155', () => { - const [tokenA, tokenB] = tokenUtils.getDummyERC20TokenAddresses(); - const tokenIdA = new BigNumber(1); - const tokenIdB = new BigNumber(2); - const valueA = new BigNumber(1); - const valueB = new BigNumber(2); - it('should succeed if ERC1155 is the same ERC1155 token and id', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES), - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES), - ); - expect(isEquivalent).to.be.true(); - }); - it('should succeed if ERC1155 is the same ERC1155 token and ids', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), - ); - expect(isEquivalent).to.be.true(); - }); - it('should succeed if ERC1155 is the same ERC1155 token and ids in different orders', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), - ); - expect(isEquivalent).to.be.true(); - }); - it('should succeed if ERC1155 is the same ERC1155 token and ids with different callback data', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], tokenA), - ); - expect(isEquivalent).to.be.true(); - }); - it('should fail if ERC1155 contains different ids', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), - assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA], [valueB], NULL_BYTES), - ); - expect(isEquivalent).to.be.false(); - }); - it('should fail if ERC1155 is a different ERC1155 token', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), - assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), - ); - expect(isEquivalent).to.be.false(); - }); - it('should fail if expected ERC1155 has different callback data', () => { - const isEquivalent = isAssetDataEquivalent( - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], tokenA), - assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), - ); - expect(isEquivalent).to.be.false(); - }); - }); - }); -}); diff --git a/packages/asset-swapper/test/wrappers.ts b/packages/asset-swapper/test/wrappers.ts index f154117835..d1d06a5805 100644 --- a/packages/asset-swapper/test/wrappers.ts +++ b/packages/asset-swapper/test/wrappers.ts @@ -38,3 +38,4 @@ export * from '../test/generated-wrappers/test_native_order_sampler'; export * from '../test/generated-wrappers/two_hop_sampler'; export * from '../test/generated-wrappers/uniswap_sampler'; export * from '../test/generated-wrappers/uniswap_v2_sampler'; +export * from '../test/generated-wrappers/utility_sampler'; diff --git a/packages/asset-swapper/tsconfig.json b/packages/asset-swapper/tsconfig.json index 251edbd301..3603aae0c0 100644 --- a/packages/asset-swapper/tsconfig.json +++ b/packages/asset-swapper/tsconfig.json @@ -39,6 +39,7 @@ "test/generated-artifacts/TestNativeOrderSampler.json", "test/generated-artifacts/TwoHopSampler.json", "test/generated-artifacts/UniswapSampler.json", - "test/generated-artifacts/UniswapV2Sampler.json" + "test/generated-artifacts/UniswapV2Sampler.json", + "test/generated-artifacts/UtilitySampler.json" ] } diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 3d856939d2..64c4b76166 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "5.10.0", + "changes": [ + { + "note": "Deploy new FQT", + "pr": 129 + } + ] + }, { "version": "5.9.0", "changes": [ diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index e84de0ca77..223a9495d2 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -37,7 +37,7 @@ "wethTransformer": "0xb2bc06a4efb20fc6553a69dbfa49b7be938034a7", "payTakerTransformer": "0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e", "affiliateFeeTransformer": "0xda6d9fc5998f550a094585cf9171f0e8ee3ac59f", - "fillQuoteTransformer": "0x5ce5174d7442061135ea849970ffc7763920e0fd" + "fillQuoteTransformer": "0xfa6282736af206cb4cfc5cb786d82aecdf1186f9" } }, "3": { @@ -78,7 +78,7 @@ "wethTransformer": "0x05ad19aa3826e0609a19568ffbd1dfe86c6c7184", "payTakerTransformer": "0x6d0ebf2bcd9cc93ec553b60ad201943dcca4e291", "affiliateFeeTransformer": "0x6588256778ca4432fa43983ac685c45efb2379e2", - "fillQuoteTransformer": "0xd67cf1088e258b13b86f5ed9a2d193a626649ede" + "fillQuoteTransformer": "0xd2a157fe2f72f5fb550826d93a9a57dcf51cc08f" } }, "4": { diff --git a/packages/migrations/CHANGELOG.json b/packages/migrations/CHANGELOG.json index a00a1f4397..da6a35a0a4 100644 --- a/packages/migrations/CHANGELOG.json +++ b/packages/migrations/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "6.6.0", + "changes": [ + { + "note": "Update `BridgeAdapter` deployment", + "pr": 104 + } + ] + }, { "version": "6.5.11", "changes": [ diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index beddf3dfc6..b77882dd98 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -310,33 +310,7 @@ export async function runMigrationsAsync( provider, txDefaults, allArtifacts, - { - balancerBridge: NULL_ADDRESS, - curveBridge: NULL_ADDRESS, - kyberBridge: NULL_ADDRESS, - mooniswapBridge: NULL_ADDRESS, - mStableBridge: NULL_ADDRESS, - oasisBridge: NULL_ADDRESS, - swerveBridge: NULL_ADDRESS, - sushiswapBridge: NULL_ADDRESS, - uniswapBridge: NULL_ADDRESS, - uniswapV2Bridge: NULL_ADDRESS, - kyberNetworkProxy: NULL_ADDRESS, - oasis: NULL_ADDRESS, - sushiswapRouter: NULL_ADDRESS, - uniswapV2Router: NULL_ADDRESS, - uniswapExchangeFactory: NULL_ADDRESS, - mStable: NULL_ADDRESS, - shellBridge: NULL_ADDRESS, - creamBridge: NULL_ADDRESS, - dodoBridge: NULL_ADDRESS, - dodoHelper: NULL_ADDRESS, - snowSwapBridge: NULL_ADDRESS, - cryptoComBridge: NULL_ADDRESS, - bancorBridge: NULL_ADDRESS, - cofixBridge: NULL_ADDRESS, - weth: etherToken.address, - }, + etherToken.address, ); const exchangeProxy = await fullMigrateExchangeProxyAsync(txDefaults.from, provider, txDefaults); diff --git a/packages/protocol-utils/CHANGELOG.json b/packages/protocol-utils/CHANGELOG.json index 7ef3abd425..df59bb48d6 100644 --- a/packages/protocol-utils/CHANGELOG.json +++ b/packages/protocol-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.2.0", + "changes": [ + { + "note": "Update transformer utils for V4 FQT", + "pr": 104 + } + ] + }, { "version": "1.1.5", "changes": [ diff --git a/packages/protocol-utils/src/orders.ts b/packages/protocol-utils/src/orders.ts index 31a5812e09..040a40279b 100644 --- a/packages/protocol-utils/src/orders.ts +++ b/packages/protocol-utils/src/orders.ts @@ -43,9 +43,18 @@ const RFQ_ORDER_DEFAULT_VALUES = { txOrigin: NULL_ADDRESS, }; +const BRIDGE_ORDER_DEFAULT_VALUES = { + source: ZERO, + takerTokenAmount: ZERO, + makerTokenAmount: ZERO, + bridgeData: '', +}; + export type CommonOrderFields = typeof COMMON_ORDER_DEFAULT_VALUES; export type LimitOrderFields = typeof LIMIT_ORDER_DEFAULT_VALUES; export type RfqOrderFields = typeof RFQ_ORDER_DEFAULT_VALUES; +export type BridgeOrderFields = typeof BRIDGE_ORDER_DEFAULT_VALUES; +export type NativeOrder = RfqOrder | LimitOrder; export enum OrderStatus { Invalid = 0, @@ -96,6 +105,12 @@ export abstract class OrderBase { return getExchangeProxyEIP712Hash(this.getStructHash(), this.chainId, this.verifyingContract); } + public willExpire(secondsFromNow: number = 0): boolean { + const millisecondsInSecond = 1000; + const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).integerValue(); + return this.expiry.isLessThan(currentUnixTimestampSec.plus(secondsFromNow)); + } + public async getSignatureWithProviderAsync( provider: SupportedProvider, type: SignatureType = SignatureType.EthSign, diff --git a/packages/protocol-utils/src/signature_utils.ts b/packages/protocol-utils/src/signature_utils.ts index 879f54fa06..fc15da4ca1 100644 --- a/packages/protocol-utils/src/signature_utils.ts +++ b/packages/protocol-utils/src/signature_utils.ts @@ -30,6 +30,16 @@ export interface Signature extends ECSignature { signatureType: SignatureType; } +/** + * ABI definition for the `Signature` struct. + */ +export const SIGNATURE_ABI = [ + { name: 'signatureType', type: 'uint8' }, + { name: 'v', type: 'uint8' }, + { name: 'r', type: 'bytes32' }, + { name: 's', type: 'bytes32' }, +]; + /** * Sign a hash with the EthSign signature type on a provider. */ diff --git a/packages/protocol-utils/src/transformer_utils.ts b/packages/protocol-utils/src/transformer_utils.ts index be16535171..d7906fb13b 100644 --- a/packages/protocol-utils/src/transformer_utils.ts +++ b/packages/protocol-utils/src/transformer_utils.ts @@ -1,22 +1,42 @@ -import { Order } from '@0x/types'; import { AbiEncoder, BigNumber, NULL_ADDRESS } from '@0x/utils'; import * as ethjs from 'ethereumjs-util'; -const ORDER_ABI_COMPONENTS = [ - { name: 'makerAddress', type: 'address' }, - { name: 'takerAddress', type: 'address' }, - { name: 'feeRecipientAddress', type: 'address' }, - { name: 'senderAddress', type: 'address' }, - { name: 'makerAssetAmount', type: 'uint256' }, - { name: 'takerAssetAmount', type: 'uint256' }, - { name: 'makerFee', type: 'uint256' }, - { name: 'takerFee', type: 'uint256' }, - { name: 'expirationTimeSeconds', type: 'uint256' }, - { name: 'salt', type: 'uint256' }, - { name: 'makerAssetData', type: 'bytes' }, - { name: 'takerAssetData', type: 'bytes' }, - { name: 'makerFeeAssetData', type: 'bytes' }, - { name: 'takerFeeAssetData', type: 'bytes' }, +import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields } from './orders'; +import { Signature, SIGNATURE_ABI } from './signature_utils'; + +const BRIDGE_ORDER_ABI_COMPONENTS = [ + { name: 'source', type: 'uint256' }, + { name: 'takerTokenAmount', type: 'uint256' }, + { name: 'makerTokenAmount', type: 'uint256' }, + { name: 'bridgeData', type: 'bytes' }, +]; + +const LIMIT_ORDER_INFO_ABI_COMPONENTS = [ + { + name: 'order', + type: 'tuple', + components: LimitOrder.STRUCT_ABI, + }, + { + name: 'signature', + type: 'tuple', + components: SIGNATURE_ABI, + }, + { name: 'maxTakerTokenFillAmount', type: 'uint256' }, +]; + +const RFQ_ORDER_INFO_ABI_COMPONENTS = [ + { + name: 'order', + type: 'tuple', + components: RfqOrder.STRUCT_ABI, + }, + { + name: 'signature', + type: 'tuple', + components: SIGNATURE_ABI, + }, + { name: 'maxTakerTokenFillAmount', type: 'uint256' }, ]; /** @@ -31,15 +51,23 @@ export const fillQuoteTransformerDataEncoder = AbiEncoder.create([ { name: 'sellToken', type: 'address' }, { name: 'buyToken', type: 'address' }, { - name: 'orders', + name: 'bridgeOrders', type: 'tuple[]', - components: ORDER_ABI_COMPONENTS, + components: BRIDGE_ORDER_ABI_COMPONENTS, }, - { name: 'signatures', type: 'bytes[]' }, - { name: 'maxOrderFillAmounts', type: 'uint256[]' }, + { + name: 'limitOrders', + type: 'tuple[]', + components: LIMIT_ORDER_INFO_ABI_COMPONENTS, + }, + { + name: 'rfqOrders', + type: 'tuple[]', + components: RFQ_ORDER_INFO_ABI_COMPONENTS, + }, + { name: 'fillSequence', type: 'uint8[]' }, { name: 'fillAmount', type: 'uint256' }, { name: 'refundReceiver', type: 'address' }, - { name: 'rfqtTakerAddress', type: 'address' }, ], }, ]); @@ -52,6 +80,15 @@ export enum FillQuoteTransformerSide { Buy, } +/** + * `FillQuoteTransformer.OrderType` + */ +export enum FillQuoteTransformerOrderType { + Bridge, + Limit, + Rfq, +} + /** * `FillQuoteTransformer.TransformData` */ @@ -59,14 +96,69 @@ export interface FillQuoteTransformerData { side: FillQuoteTransformerSide; sellToken: string; buyToken: string; - orders: Array>; - signatures: string[]; - maxOrderFillAmounts: BigNumber[]; + bridgeOrders: FillQuoteTransformerBridgeOrder[]; + limitOrders: FillQuoteTransformerLimitOrderInfo[]; + rfqOrders: FillQuoteTransformerRfqOrderInfo[]; + fillSequence: FillQuoteTransformerOrderType[]; fillAmount: BigNumber; refundReceiver: string; - rfqtTakerAddress: string; } +/** + * Identifies the DEX type of a bridge order. + */ +export enum BridgeSource { + Balancer, + Bancor, + // tslint:disable-next-line: enum-naming + CoFiX, + Curve, + Cream, + CryptoCom, + Dodo, + Kyber, + LiquidityProvider, + Mooniswap, + MStable, + Oasis, + Shell, + Snowswap, + Sushiswap, + Swerve, + Uniswap, + UniswapV2, +} + +/** + * `FillQuoteTransformer.BridgeOrder` + */ +export interface FillQuoteTransformerBridgeOrder { + source: BridgeSource; + takerTokenAmount: BigNumber; + makerTokenAmount: BigNumber; + bridgeData: string; +} + +/** + * Represents either `FillQuoteTransformer.LimitOrderInfo` + * or `FillQuoteTransformer.RfqOrderInfo` + */ +interface FillQuoteTransformerNativeOrderInfo { + order: T; + signature: Signature; + maxTakerTokenFillAmount: BigNumber; +} + +/** + * `FillQuoteTransformer.LimitOrderInfo` + */ +export type FillQuoteTransformerLimitOrderInfo = FillQuoteTransformerNativeOrderInfo; + +/** + * `FillQuoteTransformer.RfqOrderInfo` + */ +export type FillQuoteTransformerRfqOrderInfo = FillQuoteTransformerNativeOrderInfo; + /** * ABI-encode a `FillQuoteTransformer.TransformData` type. */ diff --git a/yarn.lock b/yarn.lock index b37d7fa1cb..31e67c9a9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -643,10 +643,31 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" -"@0x/abi-gen@^5.4.13", "@0x/abi-gen@^5.4.19": +"@0x/abi-gen@^5.4.13": + version "5.4.13" + resolved "https://registry.yarnpkg.com/@0x/abi-gen/-/abi-gen-5.4.13.tgz#fd7101d6937faffee1f95b6cf7bf4f589b6485cc" + dependencies: + "@0x/types" "^3.3.1" + "@0x/typescript-typings" "^5.1.6" + "@0x/utils" "^6.1.1" + "@types/node" "12.12.54" + "@types/toposort" "^2.0.1" + chalk "^2.3.0" + change-case "^3.0.2" + cli-format "^3.0.9" + ethereum-types "^3.4.0" + glob "^7.1.2" + handlebars "^4.1.2" + lodash "^4.17.11" + mkdirp "^0.5.1" + tmp "^0.0.33" + to-snake-case "^1.0.0" + toposort "^2.0.2" + yargs "^10.0.3" + +"@0x/abi-gen@^5.4.19": version "5.4.19" resolved "https://registry.yarnpkg.com/@0x/abi-gen/-/abi-gen-5.4.19.tgz#d6c9dc9eee390ec191b557f57e2c369a09f6c771" - integrity sha512-uMpaSZpVaqcK52BWuhTsmbIfCrZd1HoGtIlv2J3RIQP+wsuO+pPoQCKmV0DVaJWuklfjlhIF1x3IdYPxJdM+eA== dependencies: "@0x/types" "^3.3.1" "@0x/typescript-typings" "^5.1.6" @@ -666,17 +687,7 @@ toposort "^2.0.2" yargs "^10.0.3" -"@0x/assert@2.2.0-beta.2": - version "2.2.0-beta.2" - resolved "https://registry.yarnpkg.com/@0x/assert/-/assert-2.2.0-beta.2.tgz#de78c0bb2c9eff263df5bbe743fe73f19ca9c3bd" - dependencies: - "@0x/json-schemas" "^4.1.0-beta.2" - "@0x/typescript-typings" "^4.4.0-beta.2" - "@0x/utils" "^4.6.0-beta.2" - lodash "^4.17.11" - valid-url "^1.0.9" - -"@0x/assert@^3.0.1", "@0x/assert@^3.0.17", "@0x/assert@^3.0.6", "@0x/assert@^3.0.9": +"@0x/assert@^3.0.1", "@0x/assert@^3.0.17": version "3.0.17" resolved "https://registry.yarnpkg.com/@0x/assert/-/assert-3.0.17.tgz#dc15d038ed085744cb375044218285368f0cbfa8" dependencies: @@ -687,10 +698,9 @@ lodash "^4.17.11" valid-url "^1.0.9" -"@0x/assert@^3.0.21": +"@0x/assert@^3.0.19", "@0x/assert@^3.0.21", "@0x/assert@^3.0.6": version "3.0.21" resolved "https://registry.yarnpkg.com/@0x/assert/-/assert-3.0.21.tgz#b385868d1833625912fd9173a2477be5a4090aed" - integrity sha512-DDl8C6uYuy/Lw19HcOfDjgM2pwGafIUoePqKUwGeGRhMcARBWzr/KcBIKdBdQOVWg9CYKWFkiWyjTlAt8r6G3A== dependencies: "@0x/json-schemas" "^5.4.1" "@0x/typescript-typings" "^5.1.6" @@ -699,10 +709,26 @@ lodash "^4.17.11" valid-url "^1.0.9" -"@0x/base-contract@^6.2.14", "@0x/base-contract@^6.2.18": +"@0x/base-contract@^6.2.14": + version "6.2.14" + resolved "https://registry.yarnpkg.com/@0x/base-contract/-/base-contract-6.2.14.tgz#a4a8dfc91a7d33dbfd010841ed843a077531c9c6" + dependencies: + "@0x/assert" "^3.0.19" + "@0x/json-schemas" "^5.3.4" + "@0x/utils" "^6.1.1" + "@0x/web3-wrapper" "^7.3.0" + "@types/node" "12.12.54" + ethereumjs-account "^3.0.0" + ethereumjs-blockstream "^7.0.0" + ethereumjs-util "^5.1.1" + ethereumjs-vm "^4.2.0" + ethers "~4.0.4" + js-sha3 "^0.7.0" + uuid "^3.3.2" + +"@0x/base-contract@^6.2.18": version "6.2.18" resolved "https://registry.yarnpkg.com/@0x/base-contract/-/base-contract-6.2.18.tgz#d092de93d52d8cf8e5d4600d9ca852421322ab38" - integrity sha512-Fg7KjZQqCwDqsAdSXZQv2CXC7ujlLYFaq/sJIOM76vOkv9yyWpC8KAh5ZJ1r4bOLYCo6L8zICYD4QELBSKy5wQ== dependencies: "@0x/assert" "^3.0.21" "@0x/json-schemas" "^5.4.1" @@ -717,29 +743,29 @@ js-sha3 "^0.7.0" uuid "^3.3.2" -"@0x/connect@^6.0.9": - version "6.0.9" - resolved "https://registry.yarnpkg.com/@0x/connect/-/connect-6.0.9.tgz#b6d93021ce864ff21df4aa3da845e002d4704e4b" - dependencies: - "@0x/assert" "^3.0.9" - "@0x/json-schemas" "^5.1.0" - "@0x/types" "^3.2.0" - "@0x/typescript-typings" "^5.1.1" - "@0x/utils" "^5.5.1" - lodash "^4.17.11" - query-string "^6.0.0" - sinon "^4.0.0" - uuid "^3.3.2" - websocket "^1.0.26" - "@0x/contract-addresses@^4.0.0": version "4.12.0" resolved "https://registry.yarnpkg.com/@0x/contract-addresses/-/contract-addresses-4.12.0.tgz#2adb0bcde763ad13437f782adf25c403107ff428" -"@0x/contracts-gen@^2.0.24", "@0x/contracts-gen@^2.0.30": +"@0x/contracts-gen@^2.0.24": + version "2.0.24" + resolved "https://registry.yarnpkg.com/@0x/contracts-gen/-/contracts-gen-2.0.24.tgz#494c0867bab7681ebd1de56fe435b7366c1ef39b" + dependencies: + "@0x/sol-compiler" "^4.4.1" + "@0x/sol-resolver" "^3.1.6" + "@0x/types" "^3.3.1" + "@0x/typescript-typings" "^5.1.6" + "@0x/utils" "^6.1.1" + "@types/node" "12.12.54" + ethereum-types "^3.4.0" + lodash "^4.17.11" + mkdirp "^0.5.1" + prettier "^1.16.3" + to-snake-case "^1.0.0" + +"@0x/contracts-gen@^2.0.30": version "2.0.30" resolved "https://registry.yarnpkg.com/@0x/contracts-gen/-/contracts-gen-2.0.30.tgz#e143660b6f2aadc675e56d7675061dbd370b4985" - integrity sha512-HpGwQem6lN4n69MAINEN9sPOJ2SgQyA9w8Annr9eq7HY470kDLt8FWR7uSHqQFLBUa00fqsWXmYkluwFrL/riA== dependencies: "@0x/sol-compiler" "^4.5.2" "@0x/sol-resolver" "^3.1.6" @@ -783,7 +809,6 @@ "@0x/dev-utils@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@0x/dev-utils/-/dev-utils-4.2.1.tgz#f7364d218ae4ef9b2502963aba3cc34d83cf8673" - integrity sha512-t1mwu8FLsoydLVBODlUf9ADK8uFc599I91lND4foPgKgVixBdO2A22nLvNFJzLl3cdY/fPlx2ITL2ruuMRM6vg== dependencies: "@0x/subproviders" "^6.4.1" "@0x/types" "^3.3.1" @@ -800,16 +825,7 @@ lodash "^4.17.11" web3-provider-engine "14.0.6" -"@0x/json-schemas@^4.1.0-beta.2": - version "4.1.0-beta.3" - resolved "https://registry.yarnpkg.com/@0x/json-schemas/-/json-schemas-4.1.0-beta.3.tgz#af70a35691108ea162140640bae93a7fc84ca6ee" - dependencies: - "@0x/typescript-typings" "^4.4.0-beta.2" - "@types/node" "*" - jsonschema "^1.2.0" - lodash.values "^4.3.0" - -"@0x/json-schemas@^5.0.1", "@0x/json-schemas@^5.0.7", "@0x/json-schemas@^5.1.0", "@0x/json-schemas@^5.3.3": +"@0x/json-schemas@^5.0.1", "@0x/json-schemas@^5.0.7", "@0x/json-schemas@^5.3.3": version "5.3.3" resolved "https://registry.yarnpkg.com/@0x/json-schemas/-/json-schemas-5.3.3.tgz#4b9de100385ca23b0cd58a454165df2e9758e453" dependencies: @@ -818,29 +834,15 @@ jsonschema "^1.2.0" lodash.values "^4.3.0" -"@0x/json-schemas@^5.4.1": +"@0x/json-schemas@^5.3.4", "@0x/json-schemas@^5.4.1": version "5.4.1" resolved "https://registry.yarnpkg.com/@0x/json-schemas/-/json-schemas-5.4.1.tgz#488cae01fbb7f37fa9043e426f52ff32de69f6e0" - integrity sha512-zZ2CPrRf2RfqnMdHIVy7h3mv31sCrqav71E2CpgQt0nkgoepbsBJdIEpQfr4AYwF40EsZuSkGe5JoyP+RFyKZw== dependencies: "@0x/typescript-typings" "^5.1.6" "@types/node" "12.12.54" jsonschema "^1.2.0" lodash.values "^4.3.0" -"@0x/mesh-rpc-client@^7.0.4-beta-0xv3": - version "7.0.4-beta-0xv3" - resolved "https://registry.yarnpkg.com/@0x/mesh-rpc-client/-/mesh-rpc-client-7.0.4-beta-0xv3.tgz#5e933a0b9cf20ca900f309fc4adee03b081eb335" - dependencies: - "@0x/assert" "2.2.0-beta.2" - "@0x/types" "2.5.0-beta.2" - "@0x/typescript-typings" "4.4.0-beta.2" - "@0x/web3-providers-fork" "0.0.7" - bignumber.js "~9.0.0" - detect-node "2.0.3" - uuid "^3.3.2" - websocket "^1.0.29" - "@0x/mesh-rpc-client@^9.4.2": version "9.4.2" resolved "https://registry.yarnpkg.com/@0x/mesh-rpc-client/-/mesh-rpc-client-9.4.2.tgz#6f9690fb1cb37fb0c2fd3907241af0e543c78451" @@ -856,7 +858,6 @@ "@0x/monorepo-scripts@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@0x/monorepo-scripts/-/monorepo-scripts-3.0.5.tgz#1f1504d0d8deae3143394d9ae70980be49fceb25" - integrity sha512-QQ1y34Mj4W4PvTMVwK/PUM3O/+xUWeYGx2kHFvFKXfYuPV+OT+9vFkvDqsFM9RtRBeXbMHBNQJyZXeCtoGPtdQ== dependencies: "@0x/types" "^3.3.1" "@0x/utils" "^6.2.0" @@ -881,21 +882,9 @@ typedoc "~0.16.11" yargs "^10.0.3" -"@0x/orderbook@0xProject/gitpkg-registry#0x-orderbook-v2.2.7-e10a81023": - version "2.2.7" - resolved "https://codeload.github.com/0xProject/gitpkg-registry/tar.gz/c4b7cdf7770608fa1005a3f91c740ed417541fd9" - dependencies: - "@0x/assert" "^3.0.17" - "@0x/connect" "^6.0.9" - "@0x/contracts-dev-utils" "^1.3.5" - "@0x/mesh-rpc-client" "^7.0.4-beta-0xv3" - "@0x/order-utils" "^10.4.2" - "@0x/utils" "^6.1.0" - "@0x/quote-server@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@0x/quote-server/-/quote-server-4.0.1.tgz#05947589bfa7905d274ac3c726cb9918b93b0f9e" - integrity sha512-/5yP07a78Fqfj7IKicz57Z1R337iLYQP5qi17lMAmuIjwr7DOoUPMh0HL8FSmrf/Mk79GqC+f3jApTkXyR99fw== dependencies: "@0x/json-schemas" "^5.0.7" "@0x/order-utils" "^10.2.4" @@ -906,10 +895,36 @@ express-async-handler "^1.1.4" http-status-codes "^1.4.0" -"@0x/sol-compiler@^4.4.1", "@0x/sol-compiler@^4.5.2": +"@0x/sol-compiler@^4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@0x/sol-compiler/-/sol-compiler-4.4.1.tgz#c10a207bb7e1ab41587df1542e143b88e40b01f7" + dependencies: + "@0x/assert" "^3.0.19" + "@0x/json-schemas" "^5.3.4" + "@0x/sol-resolver" "^3.1.6" + "@0x/types" "^3.3.1" + "@0x/typescript-typings" "^5.1.6" + "@0x/utils" "^6.1.1" + "@0x/web3-wrapper" "^7.3.0" + "@types/node" "12.12.54" + "@types/yargs" "^11.0.0" + chalk "^2.3.0" + chokidar "^3.0.2" + ethereum-types "^3.4.0" + ethereumjs-util "^5.1.1" + lodash "^4.17.11" + mkdirp "^0.5.1" + pluralize "^7.0.0" + require-from-string "^2.0.1" + semver "5.5.0" + solc "^0.5.5" + source-map-support "^0.5.0" + web3-eth-abi "^1.0.0-beta.24" + yargs "^10.0.3" + +"@0x/sol-compiler@^4.5.2": version "4.5.2" resolved "https://registry.yarnpkg.com/@0x/sol-compiler/-/sol-compiler-4.5.2.tgz#da47263dfe14045c735955d5a026e1b6af7e855c" - integrity sha512-L5q4S5blMZusY7q5Unmv9GtBo0N0Sjp56F7iDl8bVzTU8F3H83WAaNkqWRzrPZKCHwAQcP3a0rNAprwCtavkeA== dependencies: "@0x/assert" "^3.0.21" "@0x/json-schemas" "^5.4.1" @@ -937,7 +952,6 @@ "@0x/sol-coverage@^4.0.29": version "4.0.29" resolved "https://registry.yarnpkg.com/@0x/sol-coverage/-/sol-coverage-4.0.29.tgz#4ce7d5d20f73f11e435497085378b214124f5ad7" - integrity sha512-uUtDEzT+kuph6F09ciJtmd/bFqF0ypViuI6qn4Axt25w255yklK6p1Xsl8CsFv86xjaWH2Y2n3/QsYtxoetWOA== dependencies: "@0x/sol-tracing-utils" "^7.1.19" "@0x/subproviders" "^6.4.1" @@ -952,7 +966,6 @@ "@0x/sol-profiler@^4.1.19": version "4.1.19" resolved "https://registry.yarnpkg.com/@0x/sol-profiler/-/sol-profiler-4.1.19.tgz#28deae3d61cfef9e748843a602d6cc096a2ab990" - integrity sha512-pFnYHVA/ncMKCxlwjOcmUP2hW+TnmaIjEoK2YDQwVpaVxJbxlTlRLVcQHGHvIQYqwyzBipfkWseqLx73kvNS2w== dependencies: "@0x/sol-tracing-utils" "^7.1.19" "@0x/subproviders" "^6.4.1" @@ -967,7 +980,6 @@ "@0x/sol-resolver@^3.1.6": version "3.1.6" resolved "https://registry.yarnpkg.com/@0x/sol-resolver/-/sol-resolver-3.1.6.tgz#ffa68dc6716b7608539e6323a7d7cf91bd869e85" - integrity sha512-VhWHhUM2vzLwI9vSq5zmPXjTgV8CSFZRRfM1d5EjBMfLtfAVpK8iMHUgulOwAV+FCGFS0SfEzfWPVaGWeCLoaA== dependencies: "@0x/types" "^3.3.1" "@0x/typescript-typings" "^5.1.6" @@ -977,7 +989,6 @@ "@0x/sol-trace@^3.0.29": version "3.0.29" resolved "https://registry.yarnpkg.com/@0x/sol-trace/-/sol-trace-3.0.29.tgz#e0d58b0b5a74c42c8c149ad37b46d40571e3c61d" - integrity sha512-i2+V5owTWkiYNBTUPgVmEQt1bpri5P/K+Gbx2GflkFwPt8HIkLi1lR636vqmrSTD+SKaHWY0zs3CXJI5i84l8A== dependencies: "@0x/sol-tracing-utils" "^7.1.19" "@0x/subproviders" "^6.4.1" @@ -993,7 +1004,6 @@ "@0x/sol-tracing-utils@^7.1.19": version "7.1.19" resolved "https://registry.yarnpkg.com/@0x/sol-tracing-utils/-/sol-tracing-utils-7.1.19.tgz#4e05144e829b4f1e4b49086c5950209ca0d71722" - integrity sha512-g4GTpLuJVH7VdH6g8otuPXQ7uvC2jBGzfGXLWd0/zs2RKUR8tbzxlOiIKhETaVADBnAqEIoUvbKn5//L52j4cw== dependencies: "@0x/dev-utils" "^4.2.1" "@0x/sol-compiler" "^4.5.2" @@ -1046,10 +1056,37 @@ optionalDependencies: "@ledgerhq/hw-transport-node-hid" "^4.3.0" -"@0x/subproviders@^6.2.3", "@0x/subproviders@^6.4.1": +"@0x/subproviders@^6.2.3": + version "6.2.3" + resolved "https://registry.yarnpkg.com/@0x/subproviders/-/subproviders-6.2.3.tgz#632ed8b73b65bc6b6d5944eee3e95dda02475629" + dependencies: + "@0x/assert" "^3.0.19" + "@0x/types" "^3.3.1" + "@0x/typescript-typings" "^5.1.6" + "@0x/utils" "^6.1.1" + "@0x/web3-wrapper" "^7.3.0" + "@ledgerhq/hw-app-eth" "^4.3.0" + "@ledgerhq/hw-transport-u2f" "4.24.0" + "@types/hdkey" "^0.7.0" + "@types/node" "12.12.54" + "@types/web3-provider-engine" "^14.0.0" + bip39 "^2.5.0" + bn.js "^4.11.8" + ethereum-types "^3.4.0" + ethereumjs-tx "^1.3.5" + ethereumjs-util "^5.1.1" + ganache-core "^2.10.2" + hdkey "^0.7.1" + json-rpc-error "2.0.0" + lodash "^4.17.11" + semaphore-async-await "^1.5.1" + web3-provider-engine "14.0.6" + optionalDependencies: + "@ledgerhq/hw-transport-node-hid" "^4.3.0" + +"@0x/subproviders@^6.4.1": version "6.4.1" resolved "https://registry.yarnpkg.com/@0x/subproviders/-/subproviders-6.4.1.tgz#aaec18651f6ae6a1c545481efa6aaebb97525998" - integrity sha512-DurwE7xQeeqfmPDU4FexmuMwK4aNfhfry/naPAYlz8aR7kKf1penZFR4h4bs2wgGhtcPnw3800qUlpFwueQRgw== dependencies: "@0x/assert" "^3.0.21" "@0x/types" "^3.3.1" @@ -1099,23 +1136,7 @@ tslint-react "^3.2.0" tsutils "3.0.0" -"@0x/types@2.5.0-beta.2": - version "2.5.0-beta.2" - resolved "https://registry.yarnpkg.com/@0x/types/-/types-2.5.0-beta.2.tgz#19d8bda61d5c1b1febc569d30dc8e7bf764d38f9" - dependencies: - "@types/node" "*" - bignumber.js "~9.0.0" - ethereum-types "^2.2.0-beta.2" - -"@0x/types@^2.5.0-beta.3": - version "2.5.0-beta.3" - resolved "https://registry.yarnpkg.com/@0x/types/-/types-2.5.0-beta.3.tgz#e010e9dbf62e37e59177c1d6df8d1acf3a9ea1b4" - dependencies: - "@types/node" "*" - bignumber.js "~9.0.0" - ethereum-types "^2.2.0-beta.2" - -"@0x/types@^3.1.0", "@0x/types@^3.1.2", "@0x/types@^3.2.0", "@0x/types@^3.3.0": +"@0x/types@^3.1.0", "@0x/types@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@0x/types/-/types-3.3.0.tgz#98c5ee91b66c7cc1719cfece6c3e5477c90bf9c5" dependencies: @@ -1123,26 +1144,15 @@ bignumber.js "~9.0.0" ethereum-types "^3.3.3" -"@0x/types@^3.3.1": +"@0x/types@^3.1.2", "@0x/types@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@0x/types/-/types-3.3.1.tgz#24f3f805d89c1235602abbef12bbeb7e92db9d63" - integrity sha512-QV5oiuW97LTrOxvRznHozkDdMP/7mU5VRT00l7YnE/PwI81Pk6MuO6cGdTvJvlqCu0wpvmKmMpLi/GmI59w/yA== dependencies: "@types/node" "12.12.54" bignumber.js "~9.0.0" ethereum-types "^3.4.0" -"@0x/typescript-typings@4.4.0-beta.2", "@0x/typescript-typings@^4.4.0-beta.2": - version "4.4.0-beta.2" - resolved "https://registry.yarnpkg.com/@0x/typescript-typings/-/typescript-typings-4.4.0-beta.2.tgz#67c621252f162914186b8f684ac5e306206c1cf2" - dependencies: - "@types/bn.js" "^4.11.0" - "@types/react" "*" - bignumber.js "~9.0.0" - ethereum-types "^2.2.0-beta.2" - popper.js "1.14.3" - -"@0x/typescript-typings@^5.0.0", "@0x/typescript-typings@^5.0.1", "@0x/typescript-typings@^5.0.2", "@0x/typescript-typings@^5.1.1", "@0x/typescript-typings@^5.1.5": +"@0x/typescript-typings@^5.0.0", "@0x/typescript-typings@^5.0.1", "@0x/typescript-typings@^5.1.5": version "5.1.5" resolved "https://registry.yarnpkg.com/@0x/typescript-typings/-/typescript-typings-5.1.5.tgz#dd0ad20ef42dad9d054886fd1da72839145b5863" dependencies: @@ -1153,10 +1163,9 @@ ethereum-types "^3.3.3" popper.js "1.14.3" -"@0x/typescript-typings@^5.1.6": +"@0x/typescript-typings@^5.0.2", "@0x/typescript-typings@^5.1.6": version "5.1.6" resolved "https://registry.yarnpkg.com/@0x/typescript-typings/-/typescript-typings-5.1.6.tgz#f6a64c1038930fc822606f543c5bce66c2113141" - integrity sha512-h7PPkvWgcyq9MEhXaTNQMu7pKkucXx6MQbXvsjTL5InZCfEG815n9w6LWxxRu7jCSYLcQhfeRDAbf3LPd6FZEg== dependencies: "@types/bn.js" "^4.11.0" "@types/node" "12.12.54" @@ -1165,25 +1174,7 @@ ethereum-types "^3.4.0" popper.js "1.14.3" -"@0x/utils@^4.6.0-beta.2": - version "4.6.0-beta.3" - resolved "https://registry.yarnpkg.com/@0x/utils/-/utils-4.6.0-beta.3.tgz#d40278916d98c48ea05821ae4987c88f032c7bff" - dependencies: - "@0x/types" "^2.5.0-beta.3" - "@0x/typescript-typings" "^4.4.0-beta.2" - "@types/node" "*" - abortcontroller-polyfill "^1.1.9" - bignumber.js "~9.0.0" - chalk "^2.3.0" - detect-node "2.0.3" - ethereum-types "^2.2.0-beta.2" - ethereumjs-util "^5.1.1" - ethers "~4.0.4" - isomorphic-fetch "2.2.1" - js-sha3 "^0.7.0" - lodash "^4.17.11" - -"@0x/utils@^5.1.0", "@0x/utils@^5.1.1", "@0x/utils@^5.4.0", "@0x/utils@^5.4.1", "@0x/utils@^5.5.1": +"@0x/utils@^5.1.0", "@0x/utils@^5.1.1", "@0x/utils@^5.4.0", "@0x/utils@^5.4.1": version "5.6.4" resolved "https://registry.yarnpkg.com/@0x/utils/-/utils-5.6.4.tgz#0158ec3243bbee444d90afbd79981321d19ccdfd" dependencies: @@ -1219,10 +1210,9 @@ js-sha3 "^0.7.0" lodash "^4.17.11" -"@0x/utils@^6.1.1", "@0x/utils@^6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@0x/utils/-/utils-6.2.0.tgz#07708d87691ac260163c01713ffac7a7f8e4c795" - integrity sha512-cVJYTHhXsaH8zgEpLxpuX9MnEEFsU8Kzpn9E6ACPlB1jsThOSTC0PPdlSkA6k7IrN5PHCaW879mkcBEcpFCWXQ== +"@0x/utils@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@0x/utils/-/utils-6.1.1.tgz#bb671283895dd967f912252e10420e02f9562281" dependencies: "@0x/types" "^3.3.1" "@0x/typescript-typings" "^5.1.6" @@ -1238,21 +1228,23 @@ js-sha3 "^0.7.0" lodash "^4.17.11" -"@0x/web3-providers-fork@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@0x/web3-providers-fork/-/web3-providers-fork-0.0.7.tgz#9cf40ebb6a2aa230283c5accb195d92594bb0aa7" +"@0x/utils@^6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@0x/utils/-/utils-6.2.0.tgz#07708d87691ac260163c01713ffac7a7f8e4c795" dependencies: - "@babel/runtime" "^7.3.1" - "@types/node" "^10.12.18" - eventemitter3 "3.1.0" + "@0x/types" "^3.3.1" + "@0x/typescript-typings" "^5.1.6" + "@types/node" "12.12.54" + abortcontroller-polyfill "^1.1.9" + bignumber.js "~9.0.0" + chalk "^2.3.0" + detect-node "2.0.3" + ethereum-types "^3.4.0" + ethereumjs-util "^5.1.1" + ethers "~4.0.4" + isomorphic-fetch "2.2.1" + js-sha3 "^0.7.0" lodash "^4.17.11" - url-parse "1.4.4" - web3-core "2.0.0-alpha" - web3-core-helpers "2.0.0-alpha" - web3-core-method "2.0.0-alpha" - web3-utils "2.0.0-alpha" - websocket "^1.0.28" - xhr2-cookies "1.1.0" "@0x/web3-wrapper@^7.2.8": version "7.2.8" @@ -1268,10 +1260,23 @@ ethers "~4.0.4" lodash "^4.17.11" -"@0x/web3-wrapper@^7.3.0", "@0x/web3-wrapper@^7.4.1": +"@0x/web3-wrapper@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@0x/web3-wrapper/-/web3-wrapper-7.3.0.tgz#7e6c7a27768f9c596d6809a8a10e1c6c0856a286" + dependencies: + "@0x/assert" "^3.0.19" + "@0x/json-schemas" "^5.3.4" + "@0x/typescript-typings" "^5.1.6" + "@0x/utils" "^6.1.1" + "@types/node" "12.12.54" + ethereum-types "^3.4.0" + ethereumjs-util "^5.1.1" + ethers "~4.0.4" + lodash "^4.17.11" + +"@0x/web3-wrapper@^7.4.1": version "7.4.1" resolved "https://registry.yarnpkg.com/@0x/web3-wrapper/-/web3-wrapper-7.4.1.tgz#196ed73eef6989ff953d5b2d610352b47748f73e" - integrity sha512-RNr9j/FtPHuAn7BxoKq2UUb1OoOIhbqHxXRLpsBBM4CU78/WjNF7zd95WB4RRnhbEK9HkdhgKb1a/lclCp84cA== dependencies: "@0x/assert" "^3.0.21" "@0x/json-schemas" "^5.4.1" @@ -1324,12 +1329,18 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.1.5", "@babel/runtime@^7.3.1": +"@babel/runtime@^7.1.5": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.1": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" + dependencies: + regenerator-runtime "^0.13.4" + "@balancer-labs/sor@0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-0.3.2.tgz#b05c63a07031c2ea13ed0d2670f5105cfaa40523" @@ -1342,7 +1353,6 @@ "@bancor/sdk@0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@bancor/sdk/-/sdk-0.2.9.tgz#2e4c168dc9d667709e3ed85eac3b15362c5676d8" - integrity sha512-u+ga5+XPCcqYx6XX0It46Sx4hW9iiDRgaVRwpWzKB3QXh6c/V/R2OljLHH18yrh0uDTLJI7R0XmJa2W8v2wvpQ== dependencies: "@types/text-encoding" "0.0.35" decimal.js "^10.2.0" @@ -2575,7 +2585,6 @@ "@types/isomorphic-fetch@^0.0.35": version "0.0.35" resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz#c1c0d402daac324582b6186b91f8905340ea3361" - integrity sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw== "@types/js-combinatorics@^0.5.29": version "0.5.32" @@ -2615,14 +2624,22 @@ version "12.12.54" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" -"@types/node@^10.12.18", "@types/node@^10.3.2": +"@types/node@^10.12.18": + version "10.17.51" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.51.tgz#639538575befbcf3d3861f95c41de8e47124d674" + +"@types/node@^10.3.2": version "10.17.44" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.44.tgz#3945e6b702cb6403f22b779c8ea9e5c3f44ead40" -"@types/node@^12.12.6", "@types/node@^12.6.1": +"@types/node@^12.12.6": version "12.19.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.3.tgz#a6e252973214079155f749e8bef99cc80af182fa" +"@types/node@^12.6.1": + version "12.19.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.16.tgz#15753af35cbef636182d8d8ca55b37c8583cecb3" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -2636,7 +2653,6 @@ "@types/prompts@^2.0.9": version "2.0.9" resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.9.tgz#19f419310eaa224a520476b19d4183f6a2b3bd8f" - integrity sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA== dependencies: "@types/node" "*" @@ -5722,7 +5738,6 @@ eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2: eth-sig-util@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-3.0.0.tgz#75133b3d7c20a5731af0690c385e184ab942b97e" - integrity sha512-4eFkMOhpGbTxBQ3AMzVf0haUX2uTur7DpWiHzWyTURa28BVJJtOkcb9Ok5TV0YvEPG61DODPW7ZUATbJTslioQ== dependencies: buffer "^5.2.1" elliptic "^6.4.0" @@ -5807,13 +5822,6 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" -ethereum-types@^2.2.0-beta.2: - version "2.2.0-beta.2" - resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-2.2.0-beta.2.tgz#0b446842474c2afacd351258ed4a2d0841f2608f" - dependencies: - "@types/node" "*" - bignumber.js "~9.0.0" - ethereum-types@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-3.3.3.tgz#b9328185034ee52efa32176eb6fd9f3e741cb231" @@ -5824,7 +5832,6 @@ ethereum-types@^3.3.3: ethereum-types@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-3.4.0.tgz#866091997c052db6a075e01e857bbfa04ca9e28b" - integrity sha512-+UIfqiS/HbQTjww7/bmkz1DI4D4Brw2R/NLsEcT2Gm8ilHgmJZAEzhp3c/J3h5+j1RnyqsAmNH9cRc8hdIeRsQ== dependencies: "@types/node" "12.12.54" bignumber.js "~9.0.0" @@ -6705,7 +6712,6 @@ ganache-core@^2.10.2: ganache-core@^2.13.2: version "2.13.2" resolved "https://registry.yarnpkg.com/ganache-core/-/ganache-core-2.13.2.tgz#27e6fc5417c10e6e76e2e646671869d7665814a3" - integrity sha512-tIF5cR+ANQz0+3pHWxHjIwHqFXcVo0Mb+kcsNhglNFALcYo49aQpnS9dqHartqPfMFjiHh/qFoD3mYK0d/qGgw== dependencies: abstract-leveldown "3.0.0" async "2.6.2" @@ -7913,7 +7919,6 @@ isomorphic-fetch@2.2.1, isomorphic-fetch@^2.2.1: isomorphic-fetch@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" - integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== dependencies: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" @@ -8213,7 +8218,6 @@ klaw@^1.0.0: kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== lazy@~1.0.11: version "1.0.11" @@ -10380,7 +10384,6 @@ prompt@^1.0.0: prompts@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" - integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== dependencies: kleur "^3.0.3" sisteransi "^1.0.5" @@ -10565,14 +10568,6 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -query-string@^6.0.0: - version "6.13.6" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.6.tgz#e5ac7c74f2a5da43fbca0b883b4f0bafba439966" - dependencies: - decode-uri-component "^0.2.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - querystringify@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -11421,7 +11416,6 @@ sinon@^4.0.0: sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== slash@^1.0.0: version "1.0.0" @@ -11625,10 +11619,6 @@ speedometer@~0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" -split-on-first@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" - split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -11715,10 +11705,6 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" -strict-uri-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" - string-editor@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/string-editor/-/string-editor-0.1.2.tgz#f5ff1b5ac4aed7ac6c2fb8de236d1551b20f61d0" @@ -12860,16 +12846,6 @@ web3-core-helpers@1.2.11: web3-eth-iban "1.2.11" web3-utils "1.2.11" -web3-core-helpers@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-2.0.0-alpha.tgz#76d720e50a6d5fbf91761a350060cc155fa9e3d3" - dependencies: - "@babel/runtime" "^7.3.1" - lodash "^4.17.11" - web3-core "2.0.0-alpha" - web3-eth-iban "2.0.0-alpha" - web3-utils "2.0.0-alpha" - web3-core-helpers@2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-2.0.0-alpha.1.tgz#d20db557fe8740578105fb6b5790eb22097d1974" @@ -12901,19 +12877,6 @@ web3-core-method@1.2.11: web3-core-subscriptions "1.2.11" web3-utils "1.2.11" -web3-core-method@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-2.0.0-alpha.tgz#453063885ab3cdd2bf63e5e586cdb3ee7d9cdfcb" - dependencies: - "@babel/runtime" "^7.3.1" - eventemitter3 "3.1.0" - lodash "^4.17.11" - rxjs "^6.4.0" - web3-core "2.0.0-alpha" - web3-core-helpers "2.0.0-alpha" - web3-core-subscriptions "2.0.0-alpha" - web3-utils "2.0.0-alpha" - web3-core-method@2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-2.0.0-alpha.1.tgz#6fd59cd229550bd08cb8922c095870fc9bf35f66" @@ -12976,14 +12939,6 @@ web3-core-subscriptions@1.2.11: underscore "1.9.1" web3-core-helpers "1.2.11" -web3-core-subscriptions@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-2.0.0-alpha.tgz#f8cb496af6e56b76bc0718213b7d777eeeacc741" - dependencies: - "@babel/runtime" "^7.3.1" - eventemitter3 "^3.1.0" - lodash "^4.17.11" - web3-core-subscriptions@2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-2.0.0-alpha.1.tgz#5c2164ce8649645f6809dcdc34626ae12ec0d19b" @@ -13013,18 +12968,6 @@ web3-core@1.2.11: web3-core-requestmanager "1.2.11" web3-utils "1.2.11" -web3-core@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-2.0.0-alpha.tgz#79722bd65e5d9e28b47e2f43638c8994ed3f2b8e" - dependencies: - "@babel/runtime" "^7.3.1" - "@types/bn.js" "^4.11.4" - "@types/node" "^12.6.1" - lodash "^4.17.11" - web3-core-method "2.0.0-alpha" - web3-providers "2.0.0-alpha" - web3-utils "2.0.0-alpha" - web3-core@2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-2.0.0-alpha.1.tgz#4bb87b860a0bbf9fd19ae380a4773e12ac3b3010" @@ -13161,14 +13104,6 @@ web3-eth-iban@1.2.11: bn.js "^4.11.9" web3-utils "1.2.11" -web3-eth-iban@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-2.0.0-alpha.tgz#87c164d964b50c000554b1c58e46dac8e2b04787" - dependencies: - "@babel/runtime" "^7.3.1" - bn.js "4.11.8" - web3-utils "2.0.0-alpha" - web3-eth-iban@2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-2.0.0-alpha.1.tgz#79f1f6061b95b5bb64eb4243a6c65dadf542077c" @@ -13348,22 +13283,6 @@ web3-providers-ws@1.2.11: web3-core-helpers "1.2.11" websocket "^1.0.31" -web3-providers@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-providers/-/web3-providers-2.0.0-alpha.tgz#6bce7f6e6d08fa874bd78214c6c54251cd7a81da" - dependencies: - "@babel/runtime" "^7.3.1" - "@types/node" "^10.12.18" - eventemitter3 "3.1.0" - lodash "^4.17.11" - url-parse "1.4.4" - web3-core "2.0.0-alpha" - web3-core-helpers "2.0.0-alpha" - web3-core-method "2.0.0-alpha" - web3-utils "2.0.0-alpha" - websocket "^1.0.28" - xhr2-cookies "1.1.0" - web3-providers@2.0.0-alpha.1, web3-providers@^2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-providers/-/web3-providers-2.0.0-alpha.1.tgz#19486aed4b014ec5d7687fac4eef2042db8b338c" @@ -13442,21 +13361,6 @@ web3-utils@1.3.0: underscore "1.9.1" utf8 "3.0.0" -web3-utils@2.0.0-alpha: - version "2.0.0-alpha" - resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-2.0.0-alpha.tgz#2e8b91f4887380672a270f8045826d72b58c12a4" - dependencies: - "@babel/runtime" "^7.3.1" - "@types/bn.js" "^4.11.4" - "@types/node" "^12.6.1" - bn.js "4.11.8" - eth-lib "0.2.8" - ethjs-unit "^0.1.6" - lodash "^4.17.11" - number-to-bn "1.7.0" - randombytes "^2.1.0" - utf8 "2.1.1" - web3-utils@2.0.0-alpha.1: version "2.0.0-alpha.1" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-2.0.0-alpha.1.tgz#231442adea3b58bca0c7185ee5b7743c01938682" @@ -13500,7 +13404,7 @@ webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" -websocket@1.0.32, websocket@^1.0.25, websocket@^1.0.26, websocket@^1.0.28, websocket@^1.0.29, websocket@^1.0.31: +websocket@1.0.32, websocket@^1.0.25, websocket@^1.0.31: version "1.0.32" resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.32.tgz#1f16ddab3a21a2d929dec1687ab21cfdc6d3dbb1" dependencies: @@ -13511,6 +13415,17 @@ websocket@1.0.32, websocket@^1.0.25, websocket@^1.0.26, websocket@^1.0.28, webso utf-8-validate "^5.0.2" yaeti "^0.0.6" +websocket@^1.0.29: + version "1.0.33" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.33.tgz#407f763fc58e74a3fa41ca3ae5d78d3f5e3b82a5" + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + "websocket@github:web3-js/WebSocket-Node#polyfill/globalThis": version "1.0.29" resolved "https://codeload.github.com/web3-js/WebSocket-Node/tar.gz/ef5ea2f41daf4a2113b80c9223df884b4d56c400" @@ -13532,7 +13447,6 @@ whatwg-fetch@>=0.10.0: whatwg-fetch@^3.4.1: version "3.5.0" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz#605a2cd0a7146e5db141e29d1c62ab84c0c4c868" - integrity sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A== whatwg-url@^7.0.0: version "7.1.0"