diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 04d02678af..f36a7773a7 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "4.4.0", + "changes": [ + { + "note": "Add support for ERC721 assets", + "pr": 2491 + }, + { + "note": "Add destroy for gas heartbeat", + "pr": 2492 + } + ] + }, { "version": "4.3.2", "changes": [ diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index c355dbd061..f5476b1fbd 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -234,6 +234,7 @@ export enum SwapQuoterError { InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY', AssetUnavailable = 'ASSET_UNAVAILABLE', NoGasPriceProvidedOrEstimated = 'NO_GAS_PRICE_PROVIDED_OR_ESTIMATED', + AssetDataUnsupported = 'ASSET_DATA_UNSUPPORTED', } /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts b/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts index 0d4461948d..f1b6f9004c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts @@ -1,5 +1,6 @@ import { ContractAddresses } from '@0x/contract-addresses'; import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; +import { SignedOrder } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { constants } from '../../constants'; @@ -20,6 +21,22 @@ const { INFINITE_TIMESTAMP_SEC, WALLET_SIGNATURE } = marketOperationUtilConstant export class CreateOrderUtils { private readonly _contractAddress: ContractAddresses; + // utility function for asset-swapper to ignore market operation utils for specific asset types + public static convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder { + return { + ...order, + fillableMakerAssetAmount: order.makerAssetAmount, + fillableTakerAssetAmount: order.takerAssetAmount, + fillableTakerFeeAmount: order.takerFee, + fill: { + source: ERC20BridgeSource.Native, + totalMakerAssetAmount: order.makerAssetAmount, + totalTakerAssetAmount: order.takerAssetAmount, + subFills: [], + }, + }; + } + constructor(contractAddress: ContractAddresses) { this._contractAddress = contractAddress; } diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 2e79affe24..eb3c59396f 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -1,5 +1,5 @@ -import { orderCalculationUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; +import { assetDataUtils, orderCalculationUtils } from '@0x/order-utils'; +import { AssetProxyId, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; @@ -14,10 +14,12 @@ import { SwapQuoteBase, SwapQuoteInfo, SwapQuoteOrdersBreakdown, + SwapQuoterError, } from '../types'; import { fillableAmountsUtils } from './fillable_amounts_utils'; import { MarketOperationUtils } from './market_operation_utils'; +import { CreateOrderUtils } from './market_operation_utils/create_order'; import { ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types'; import { ProtocolFeeUtils } from './protocol_fee_utils'; import { utils } from './utils'; @@ -126,6 +128,10 @@ export class SwapQuoteCalculator { operation: MarketOperation, opts: CalculateSwapQuoteOpts, ): Promise { + // checks if maker asset is ERC721 or ERC20 and taker asset is ERC20 + if (!utils.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 const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue(); @@ -137,18 +143,30 @@ export class SwapQuoteCalculator { ...opts, fees: _.mapValues(opts.fees, (v, k) => v.times(gasPrice)), }; - if (operation === MarketOperation.Buy) { - resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( - prunedOrders, - assetFillAmount.plus(slippageBufferAmount), - _opts, + + const firstOrderMakerAssetData = !!prunedOrders[0] + ? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData) + : { assetProxyId: '' }; + + if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { + // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable + resultOrders = prunedOrders.map(o => + CreateOrderUtils.convertNativeOrderToFullyFillableOptimizedOrders(o), ); } else { - resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync( - prunedOrders, - assetFillAmount.plus(slippageBufferAmount), - _opts, - ); + if (operation === MarketOperation.Buy) { + resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( + prunedOrders, + assetFillAmount.plus(slippageBufferAmount), + _opts, + ); + } else { + resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync( + prunedOrders, + assetFillAmount.plus(slippageBufferAmount), + _opts, + ); + } } } diff --git a/packages/asset-swapper/src/utils/utils.ts b/packages/asset-swapper/src/utils/utils.ts index 52d59d36b3..9bb80bb803 100644 --- a/packages/asset-swapper/src/utils/utils.ts +++ b/packages/asset-swapper/src/utils/utils.ts @@ -1,5 +1,5 @@ import { assetDataUtils } from '@0x/order-utils'; -import { AssetData, ERC20AssetData, ERC20BridgeAssetData, Order } from '@0x/types'; +import { AssetData, AssetProxyId, ERC20AssetData, ERC20BridgeAssetData, Order, SignedOrder } from '@0x/types'; import { BigNumber, NULL_BYTES } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; @@ -7,6 +7,21 @@ import { constants } from '../constants'; // tslint:disable:no-unnecessary-type-assertion export const utils = { + 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 || + makerAssetData.assetProxyId === AssetProxyId.ERC721) && + takerAssetData.assetProxyId === AssetProxyId.ERC20 && + firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId + ); // checks that all native order maker assets are of the same type + }); + }, numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber { return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy( percentage, diff --git a/packages/instant/CHANGELOG.json b/packages/instant/CHANGELOG.json index e52a729968..bd78ca6931 100644 --- a/packages/instant/CHANGELOG.json +++ b/packages/instant/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "4.2.0", + "changes": [ + { + "note": "Clean up heartbeat functions on close", + "pr": 2492 + }, + { + "note": "Fix ERC721 asset support", + "pr": 2491 + } + ] + }, { "version": "4.1.0", "changes": [ diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index a4ac89b0f6..6ba9288d0d 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -72,7 +72,9 @@ export class InstantHeading extends React.PureComponent overflow="hidden" borderRadius="50%" > - + + +