diff options
Diffstat (limited to 'packages')
20 files changed, 270 insertions, 145 deletions
diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 80437c10e..34e2d9639 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -132,9 +132,14 @@ export class AssetBuyer { assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh); assert.isNumber('slippagePercentage', slippagePercentage); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); + const isMakerAssetZrxToken = assetData === zrxTokenAssetData; + // get the relevant orders for the makerAsset and fees + // if the requested assetData is ZRX, don't get the fee info const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([ this._getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh), - this._getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh), + isMakerAssetZrxToken + ? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS) + : this._getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh), shouldForceOrderRefresh, ]); if (ordersAndFillableAmounts.orders.length === 0) { @@ -146,6 +151,7 @@ export class AssetBuyer { assetBuyAmount, feePercentage, slippagePercentage, + isMakerAssetZrxToken, ); return buyQuote; } diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index 55effdb23..c912253bd 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -1,6 +1,7 @@ +import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { AssetBuyerOpts, BuyQuoteExecutionOpts, BuyQuoteRequestOpts } from './types'; +import { AssetBuyerOpts, BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrdersAndFillableAmounts } from './types'; const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; const MAINNET_NETWORK_ID = 1; @@ -22,6 +23,11 @@ const DEFAULT_BUY_QUOTE_EXECUTION_OPTS: BuyQuoteExecutionOpts = { feeRecipient: NULL_ADDRESS, }; +const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = { + orders: [] as SignedOrder[], + remainingFillableMakerAssetAmounts: [] as BigNumber[], +}; + export const constants = { ZERO_AMOUNT: new BigNumber(0), NULL_ADDRESS, @@ -31,4 +37,5 @@ export const constants = { DEFAULT_BUY_QUOTE_EXECUTION_OPTS, DEFAULT_BUY_QUOTE_REQUEST_OPTS, MAX_PER_PAGE: 10000, + EMPTY_ORDERS_AND_FILLABLE_AMOUNTS, }; diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts index 91682b089..41fd1fb30 100644 --- a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts +++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts @@ -30,7 +30,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider { 'remainingTakerAssetAmount', order.takerAssetAmount, ); - const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + const remainingFillableMakerAssetAmount = orderUtils.getRemainingMakerAmount( order, remainingFillableTakerAssetAmount, ); diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index b0cb4a547..f94ab3fa4 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -1,10 +1,12 @@ -import { marketUtils, rateUtils } from '@0x/order-utils'; +import { marketUtils, SignedOrder } from '@0x/order-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { constants } from '../constants'; import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types'; +import { orderUtils } from './order_utils'; + // Calculates a buy quote for orders that have WETH as the takerAsset export const buyQuoteCalculator = { calculate( @@ -13,6 +15,7 @@ export const buyQuoteCalculator = { assetBuyAmount: BigNumber, feePercentage: number, slippagePercentage: number, + isMakerAssetZrxToken: boolean, ): BuyQuote { const orders = ordersAndFillableAmounts.orders; const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; @@ -32,22 +35,31 @@ export const buyQuoteCalculator = { if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { throw new Error(AssetBuyerError.InsufficientAssetLiquidity); } + // if we are not buying ZRX: // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage) // TODO(bmillman): optimization // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage - const { - resultFeeOrders, - remainingFeeAmount, - feeOrdersRemainingFillableMakerAssetAmounts, - } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(resultOrders, feeOrders, { - remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }); - // if we do not have enough feeOrders to cover the fees, throw - if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(AssetBuyerError.InsufficientZrxLiquidity); + let resultFeeOrders = [] as SignedOrder[]; + let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[]; + if (!isMakerAssetZrxToken) { + const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + resultOrders, + feeOrders, + { + remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }, + ); + // if we do not have enough feeOrders to cover the fees, throw + if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientZrxLiquidity); + } + resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders; + feeOrdersRemainingFillableMakerAssetAmounts = + feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts; } + // assetData information for the result const assetData = orders[0].makerAssetData; // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount @@ -64,6 +76,7 @@ export const buyQuoteCalculator = { trimmedFeeOrdersAndFillableAmounts, assetBuyAmount, feePercentage, + isMakerAssetZrxToken, ); // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate const worstCaseQuoteInfo = calculateQuoteInfo( @@ -71,6 +84,7 @@ export const buyQuoteCalculator = { reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts), assetBuyAmount, feePercentage, + isMakerAssetZrxToken, ); return { assetData, @@ -89,22 +103,30 @@ function calculateQuoteInfo( feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, assetBuyAmount: BigNumber, feePercentage: number, + isMakerAssetZrxToken: boolean, ): BuyQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right - const [ethAmountToBuyAsset, zrxAmountToBuyAsset] = findEthAndZrxAmountNeededToBuyAsset( - ordersAndFillableAmounts, - assetBuyAmount, - ); - // find the total eth needed to buy fees - const ethAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); - const affiliateFeeEthAmount = ethAmountToBuyAsset.mul(feePercentage); - const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees); - const totalEthAmount = totalEthAmountWithoutAffiliateFee.plus(affiliateFeeEthAmount); + let ethAmountToBuyAsset = constants.ZERO_AMOUNT; + let ethAmountToBuyZrx = constants.ZERO_AMOUNT; + if (isMakerAssetZrxToken) { + ethAmountToBuyAsset = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount); + } else { + // find eth and zrx amounts needed to buy + const ethAndZrxAmountToBuyAsset = findEthAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, assetBuyAmount); + ethAmountToBuyAsset = ethAndZrxAmountToBuyAsset[0]; + const zrxAmountToBuyAsset = ethAndZrxAmountToBuyAsset[1]; + // find eth amount needed to buy zrx + ethAmountToBuyZrx = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); + } + /// find the eth amount needed to buy the affiliate fee + const ethAmountToBuyAffiliateFee = ethAmountToBuyAsset.mul(feePercentage); + const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyZrx); + const ethAmountTotal = totalEthAmountWithoutAffiliateFee.plus(ethAmountToBuyAffiliateFee); // divide into the assetBuyAmount in order to find rate of makerAsset / WETH const ethPerAssetPrice = totalEthAmountWithoutAffiliateFee.div(assetBuyAmount); return { - totalEthAmount, - feeEthAmount: affiliateFeeEthAmount, + totalEthAmount: ethAmountTotal, + feeEthAmount: ethAmountToBuyAffiliateFee, ethPerAssetPrice, }; } @@ -119,29 +141,38 @@ function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFill }; } -function findEthAmountNeededToBuyFees( +function findEthAmountNeededToBuyZrx( feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, - feeAmount: BigNumber, + zrxBuyAmount: BigNumber, ): BigNumber { const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts; const result = _.reduce( orders, (acc, order, index) => { + const { totalEthAmount, remainingZrxBuyAmount } = acc; const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; - const amountToFill = BigNumber.min(acc.remainingFeeAmount, remainingFillableMakerAssetAmount); - const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfFeeOrder(order); - const ethAmountForThisOrder = feeAdjustedRate.mul(amountToFill); + const makerFillAmount = BigNumber.min(remainingZrxBuyAmount, remainingFillableMakerAssetAmount); + const [takerFillAmount, adjustedMakerFillAmount] = orderUtils.getTakerFillAmountForFeeOrder( + order, + makerFillAmount, + ); + const extraFeeAmount = remainingFillableMakerAssetAmount.greaterThanOrEqualTo(adjustedMakerFillAmount) + ? constants.ZERO_AMOUNT + : adjustedMakerFillAmount.sub(makerFillAmount); return { - ethAmount: acc.ethAmount.plus(ethAmountForThisOrder), - remainingFeeAmount: BigNumber.max(constants.ZERO_AMOUNT, acc.remainingFeeAmount.minus(amountToFill)), + totalEthAmount: totalEthAmount.plus(takerFillAmount), + remainingZrxBuyAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingZrxBuyAmount.minus(makerFillAmount).plus(extraFeeAmount), + ), }; }, { - ethAmount: constants.ZERO_AMOUNT, - remainingFeeAmount: feeAmount, + totalEthAmount: constants.ZERO_AMOUNT, + remainingZrxBuyAmount: zrxBuyAmount, }, ); - return result.ethAmount; + return result.totalEthAmount; } function findEthAndZrxAmountNeededToBuyAsset( @@ -152,28 +183,25 @@ function findEthAndZrxAmountNeededToBuyAsset( const result = _.reduce( orders, (acc, order, index) => { + const { totalEthAmount, totalZrxAmount, remainingAssetBuyAmount } = acc; const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; - const amountToFill = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount); - // find the amount of eth required to fill amountToFill (amountToFill / makerAssetAmount) * takerAssetAmount - const ethAmountForThisOrder = amountToFill - .mul(order.takerAssetAmount) - .dividedToIntegerBy(order.makerAssetAmount); - // find the amount of zrx required to fill fees for amountToFill (amountToFill / makerAssetAmount) * takerFee - const zrxAmountForThisOrder = amountToFill.mul(order.takerFee).dividedToIntegerBy(order.makerAssetAmount); + const makerFillAmount = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount); + const takerFillAmount = orderUtils.getTakerFillAmount(order, makerFillAmount); + const takerFeeAmount = orderUtils.getTakerFeeAmount(order, takerFillAmount); return { - ethAmount: acc.ethAmount.plus(ethAmountForThisOrder), - zrxAmount: acc.zrxAmount.plus(zrxAmountForThisOrder), + totalEthAmount: totalEthAmount.plus(takerFillAmount), + totalZrxAmount: totalZrxAmount.plus(takerFeeAmount), remainingAssetBuyAmount: BigNumber.max( constants.ZERO_AMOUNT, - acc.remainingAssetBuyAmount.minus(amountToFill), + remainingAssetBuyAmount.minus(makerFillAmount), ), }; }, { - ethAmount: constants.ZERO_AMOUNT, - zrxAmount: constants.ZERO_AMOUNT, + totalEthAmount: constants.ZERO_AMOUNT, + totalZrxAmount: constants.ZERO_AMOUNT, remainingAssetBuyAmount: assetBuyAmount, }, ); - return [result.ethAmount, result.zrxAmount]; + return [result.totalEthAmount, result.totalZrxAmount]; } diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts index 81464d945..28f684f3c 100644 --- a/packages/asset-buyer/src/utils/order_provider_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -110,10 +110,7 @@ function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( traderInfo.makerZrxBalance, ]); const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); - const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( - order, - remainingTakerAssetAmount, - ); + const remainingMakerAssetAmount = orderUtils.getRemainingMakerAmount(order, remainingTakerAssetAmount); const remainingFillableCalculator = new RemainingFillableCalculator( order.makerFee, order.makerAssetAmount, diff --git a/packages/asset-buyer/src/utils/order_utils.ts b/packages/asset-buyer/src/utils/order_utils.ts index ff47eb7c5..1cc2cf95f 100644 --- a/packages/asset-buyer/src/utils/order_utils.ts +++ b/packages/asset-buyer/src/utils/order_utils.ts @@ -12,19 +12,63 @@ export const orderUtils = { const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round(); return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec.plus(secondsFromNow)); }, - calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { - if (remainingTakerAssetAmount.eq(0)) { - return constants.ZERO_AMOUNT; - } - return remainingTakerAssetAmount.times(order.makerAssetAmount).dividedToIntegerBy(order.takerAssetAmount); - }, - calculateRemainingTakerAssetAmount(order: SignedOrder, remainingMakerAssetAmount: BigNumber): BigNumber { - if (remainingMakerAssetAmount.eq(0)) { - return constants.ZERO_AMOUNT; - } - return remainingMakerAssetAmount.times(order.takerAssetAmount).dividedToIntegerBy(order.makerAssetAmount); - }, isOpenOrder(order: SignedOrder): boolean { return order.takerAddress === constants.NULL_ADDRESS; }, + // given a remaining amount of takerAsset, calculate how much makerAsset is available + getRemainingMakerAmount(order: SignedOrder, remainingTakerAmount: BigNumber): BigNumber { + const remainingMakerAmount = remainingTakerAmount + .times(order.makerAssetAmount) + .div(order.takerAssetAmount) + .floor(); + return remainingMakerAmount; + }, + // given a desired amount of makerAsset, calculate how much takerAsset is required to fill that amount + getTakerFillAmount(order: SignedOrder, makerFillAmount: BigNumber): BigNumber { + // Round up because exchange rate favors Maker + const takerFillAmount = makerFillAmount + .mul(order.takerAssetAmount) + .div(order.makerAssetAmount) + .ceil(); + return takerFillAmount; + }, + // given a desired amount of takerAsset to fill, calculate how much fee is required by the taker to fill that amount + getTakerFeeAmount(order: SignedOrder, takerFillAmount: BigNumber): BigNumber { + // Round down because Taker fee rate favors Taker + const takerFeeAmount = takerFillAmount + .mul(order.takerFee) + .div(order.takerAssetAmount) + .floor(); + return takerFeeAmount; + }, + // given a desired amount of takerAsset to fill, calculate how much makerAsset will be filled + getMakerFillAmount(order: SignedOrder, takerFillAmount: BigNumber): BigNumber { + // Round down because exchange rate favors Maker + const makerFillAmount = takerFillAmount + .mul(order.makerAssetAmount) + .div(order.takerAssetAmount) + .floor(); + return makerFillAmount; + }, + // given a desired amount of makerAsset, calculate how much fee is required by the maker to fill that amount + getMakerFeeAmount(order: SignedOrder, makerFillAmount: BigNumber): BigNumber { + // Round down because Maker fee rate favors Maker + const makerFeeAmount = makerFillAmount + .mul(order.makerFee) + .div(order.makerAssetAmount) + .floor(); + return makerFeeAmount; + }, + // given a desired amount of ZRX from a fee order, calculate how much takerAsset is required to fill that amount + // also calculate how much ZRX needs to be bought in order fill the desired amount + takerFee + getTakerFillAmountForFeeOrder(order: SignedOrder, makerFillAmount: BigNumber): [BigNumber, BigNumber] { + // For each unit of TakerAsset we buy (MakerAsset - TakerFee) + const adjustedTakerFillAmount = makerFillAmount + .mul(order.takerAssetAmount) + .div(order.makerAssetAmount.sub(order.takerFee)) + .ceil(); + // The amount that we buy will be greater than makerFillAmount, since we buy some amount for fees. + const adjustedMakerFillAmount = orderUtils.getMakerFillAmount(order, adjustedTakerFillAmount); + return [adjustedTakerFillAmount, adjustedMakerFillAmount]; + }, }; diff --git a/packages/asset-buyer/test/buy_quote_calculator_test.ts b/packages/asset-buyer/test/buy_quote_calculator_test.ts index 0f516a0f7..0ea371982 100644 --- a/packages/asset-buyer/test/buy_quote_calculator_test.ts +++ b/packages/asset-buyer/test/buy_quote_calculator_test.ts @@ -49,9 +49,9 @@ describe('buyQuoteCalculator', () => { remainingFillableMakerAssetAmounts: [smallFeeOrder.makerAssetAmount], }; const largeFeeOrder = orderFactory.createSignedOrderFromPartial({ - makerAssetAmount: new BigNumber(110), + makerAssetAmount: new BigNumber(113), takerAssetAmount: new BigNumber(200), - takerFee: new BigNumber(10), + takerFee: new BigNumber(11), }); allFeeOrdersAndFillableAmounts = { orders: [smallFeeOrder, largeFeeOrder], @@ -70,6 +70,7 @@ describe('buyQuoteCalculator', () => { new BigNumber(500), 0, 0, + false, ), ).to.throw(AssetBuyerError.InsufficientAssetLiquidity); }); @@ -82,6 +83,7 @@ describe('buyQuoteCalculator', () => { new BigNumber(300), 0, 0, + false, ), ).to.throw(AssetBuyerError.InsufficientZrxLiquidity); }); @@ -97,6 +99,7 @@ describe('buyQuoteCalculator', () => { assetBuyAmount, feePercentage, slippagePercentage, + false, ); // test if orders are correct expect(buyQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]); @@ -134,6 +137,7 @@ describe('buyQuoteCalculator', () => { assetBuyAmount, feePercentage, slippagePercentage, + false, ); // test if orders are correct expect(buyQuote.orders).to.deep.equal(ordersAndFillableAmounts.orders); @@ -149,9 +153,9 @@ describe('buyQuoteCalculator', () => { expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); - // 100 eth to fill the first order + 200 eth for fees + // 100 eth to fill the first order + 208 eth for fees const expectedWorstEthAmountForAsset = new BigNumber(100); - const expectedWorstEthAmountForZrxFees = new BigNumber(200); + const expectedWorstEthAmountForZrxFees = new BigNumber(208); const expectedWorstFillEthAmount = expectedWorstEthAmountForAsset.plus(expectedWorstEthAmountForZrxFees); const expectedWorstFeeEthAmount = expectedWorstEthAmountForAsset.mul(feePercentage); const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.plus(expectedWorstFeeEthAmount); diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 9c42f3d87..a70269dde 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,7 +1,8 @@ -import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; import * as _ from 'lodash'; import * as React from 'react'; +import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants'; import { ColorOption } from '../style/theme'; import { util } from '../util/util'; import { web3Wrapper } from '../util/web3_wrapper'; @@ -11,9 +12,11 @@ import { Button, Text } from './ui'; export interface BuyButtonProps { buyQuote?: BuyQuote; assetBuyer?: AssetBuyer; - onClick: (buyQuote: BuyQuote) => void; - onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; - onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void; + onAwaitingSignature: (buyQuote: BuyQuote) => void; + onSignatureDenied: (buyQuote: BuyQuote, preventedError: Error) => void; + onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void; + onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void; + onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; } export class BuyButton extends React.Component<BuyButtonProps> { @@ -34,17 +37,33 @@ export class BuyButton extends React.Component<BuyButtonProps> { } private readonly _handleClick = async () => { // The button is disabled when there is no buy quote anyway. - if (_.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer)) { + const { buyQuote, assetBuyer } = this.props; + if (_.isUndefined(buyQuote) || _.isUndefined(assetBuyer)) { return; } - this.props.onClick(this.props.buyQuote); - let txnHash; + + let txHash: string | undefined; + this.props.onAwaitingSignature(buyQuote); + try { + txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote); + } catch (e) { + if (e instanceof Error && e.message === AssetBuyerError.SignatureRequestDenied) { + this.props.onSignatureDenied(buyQuote, e); + return; + } + throw e; + } + + this.props.onBuyProcessing(buyQuote, txHash); try { - txnHash = await this.props.assetBuyer.executeBuyQuoteAsync(this.props.buyQuote); - const txnReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txnHash); - this.props.onBuySuccess(this.props.buyQuote, txnReceipt.transactionHash); - } catch { - this.props.onBuyFailure(this.props.buyQuote, txnHash); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + } catch (e) { + if (e instanceof Error && e.message.startsWith(WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX)) { + this.props.onBuyFailure(buyQuote, txHash); + return; + } + throw e; } + this.props.onBuySuccess(buyQuote, txHash); }; } diff --git a/packages/instant/src/components/buy_order_state_button.tsx b/packages/instant/src/components/buy_order_state_button.tsx index 5bc965c7d..44115e5a1 100644 --- a/packages/instant/src/components/buy_order_state_button.tsx +++ b/packages/instant/src/components/buy_order_state_button.tsx @@ -4,18 +4,21 @@ import { PlacingOrderButton } from '../components/placing_order_button'; import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button'; import { SelectedAssetRetryButton } from '../containers/selected_asset_retry_button'; import { SelectedAssetViewTransactionButton } from '../containers/selected_asset_view_transaction_button'; -import { AsyncProcessState } from '../types'; +import { OrderProcessState } from '../types'; export interface BuyOrderStateButtonProps { - buyOrderProcessingState: AsyncProcessState; + buyOrderProcessingState: OrderProcessState; } export const BuyOrderStateButton: React.StatelessComponent<BuyOrderStateButtonProps> = props => { - if (props.buyOrderProcessingState === AsyncProcessState.FAILURE) { + if (props.buyOrderProcessingState === OrderProcessState.FAILURE) { return <SelectedAssetRetryButton />; - } else if (props.buyOrderProcessingState === AsyncProcessState.SUCCESS) { + } else if ( + props.buyOrderProcessingState === OrderProcessState.SUCCESS || + props.buyOrderProcessingState === OrderProcessState.PROCESSING + ) { return <SelectedAssetViewTransactionButton />; - } else if (props.buyOrderProcessingState === AsyncProcessState.PENDING) { + } else if (props.buyOrderProcessingState === OrderProcessState.AWAITING_SIGNATURE) { return <PlacingOrderButton />; } diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index ed753a3bd..17ac65429 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -4,12 +4,13 @@ import * as React from 'react'; import { SelectedAssetAmountInput } from '../containers/selected_asset_amount_input'; import { ColorOption } from '../style/theme'; -import { AsyncProcessState, OrderState } from '../types'; +import { AsyncProcessState, OrderProcessState, OrderState } from '../types'; import { format } from '../util/format'; import { AmountPlaceholder } from './amount_placeholder'; import { Container, Flex, Text } from './ui'; import { Icon } from './ui/icon'; +import { Spinner } from './ui/spinner'; export interface InstantHeadingProps { selectedAssetAmount?: BigNumber; @@ -68,9 +69,11 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { private _renderIcon(): React.ReactNode { const processState = this.props.buyOrderState.processState; - if (processState === AsyncProcessState.FAILURE) { + if (processState === OrderProcessState.FAILURE) { return <Icon icon={'failed'} width={ICON_WIDTH} height={ICON_HEIGHT} color={ICON_COLOR} />; - } else if (processState === AsyncProcessState.SUCCESS) { + } else if (processState === OrderProcessState.PROCESSING) { + return <Spinner widthPx={ICON_HEIGHT} heightPx={ICON_HEIGHT} />; + } else if (processState === OrderProcessState.SUCCESS) { return <Icon icon={'success'} width={ICON_WIDTH} height={ICON_HEIGHT} color={ICON_COLOR} />; } return undefined; @@ -78,9 +81,11 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { private _renderTopText(): React.ReactNode { const processState = this.props.buyOrderState.processState; - if (processState === AsyncProcessState.FAILURE) { + if (processState === OrderProcessState.FAILURE) { return 'Order failed'; - } else if (processState === AsyncProcessState.SUCCESS) { + } else if (processState === OrderProcessState.PROCESSING) { + return 'Processing Order...'; + } else if (processState === OrderProcessState.SUCCESS) { return 'Tokens received!'; } diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 31491c80a..48d0d4aa2 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -2,3 +2,4 @@ import { BigNumber } from '@0x/utils'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ethDecimals = 18; export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; +export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction failed'; diff --git a/packages/instant/src/containers/selected_asset_amount_input.ts b/packages/instant/src/containers/selected_asset_amount_input.ts index f23b2010e..e9dbc61ce 100644 --- a/packages/instant/src/containers/selected_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_asset_amount_input.ts @@ -10,7 +10,7 @@ import { Dispatch } from 'redux'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; -import { AsyncProcessState, ERC20Asset } from '../types'; +import { ERC20Asset, OrderProcessState } from '../types'; import { errorUtil } from '../util/error'; import { AssetAmountInput } from '../components/asset_amount_input'; @@ -90,7 +90,7 @@ const mapDispatchToProps = ( // invalidate the last buy quote. dispatch(actions.updateLatestBuyQuote(undefined)); // reset our buy state - dispatch(actions.updateBuyOrderState({ processState: AsyncProcessState.NONE })); + dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.NONE })); if (!_.isUndefined(value) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) { // even if it's debounced, give them the illusion it's loading diff --git a/packages/instant/src/containers/selected_asset_buy_button.ts b/packages/instant/src/containers/selected_asset_buy_button.ts index 71d2b8cf0..adcbd61bc 100644 --- a/packages/instant/src/containers/selected_asset_buy_button.ts +++ b/packages/instant/src/containers/selected_asset_buy_button.ts @@ -6,7 +6,7 @@ import { Dispatch } from 'redux'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; -import { AsyncProcessState } from '../types'; +import { OrderProcessState, OrderState } from '../types'; import { BuyButton } from '../components/buy_button'; @@ -18,9 +18,11 @@ interface ConnectedState { } interface ConnectedDispatch { - onClick: (buyQuote: BuyQuote) => void; - onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; - onBuyFailure: (buyQuote: BuyQuote) => void; + onAwaitingSignature: (buyQuote: BuyQuote) => void; + onSignatureDenied: (buyQuote: BuyQuote, error: Error) => void; + onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void; + onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void; + onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; } const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ @@ -29,10 +31,22 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): }); const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: SelectedAssetBuyButtonProps): ConnectedDispatch => ({ - onClick: buyQuote => dispatch(actions.updateBuyOrderState({ processState: AsyncProcessState.PENDING })), - onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => - dispatch(actions.updateBuyOrderState({ processState: AsyncProcessState.SUCCESS, txnHash })), - onBuyFailure: buyQuote => dispatch(actions.updateBuyOrderState({ processState: AsyncProcessState.FAILURE })), + onAwaitingSignature: (buyQuote: BuyQuote) => { + const newOrderState: OrderState = { processState: OrderProcessState.AWAITING_SIGNATURE }; + dispatch(actions.updateBuyOrderState(newOrderState)); + }, + onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => { + const newOrderState: OrderState = { processState: OrderProcessState.PROCESSING, txHash }; + dispatch(actions.updateBuyOrderState(newOrderState)); + }, + onBuySuccess: (buyQuote: BuyQuote, txHash: string) => + dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.SUCCESS, txHash })), + onBuyFailure: (buyQuote: BuyQuote, txHash: string) => + dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.FAILURE, txHash })), + onSignatureDenied: (buyQuote, error) => { + dispatch(actions.resetAmount()); + dispatch(actions.setError(error)); + }, }); export const SelectedAssetBuyButton: React.ComponentClass<SelectedAssetBuyButtonProps> = connect( diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx b/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx index f3efbb5d2..7faa79912 100644 --- a/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx +++ b/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { State } from '../redux/reducer'; -import { AsyncProcessState } from '../types'; +import { OrderProcessState } from '../types'; import { BuyOrderStateButton } from '../components/buy_order_state_button'; interface ConnectedState { - buyOrderProcessingState: AsyncProcessState; + buyOrderProcessingState: OrderProcessState; } export interface SelectedAssetButtonProps {} const mapStateToProps = (state: State, _ownProps: SelectedAssetButtonProps): ConnectedState => ({ diff --git a/packages/instant/src/containers/selected_asset_view_transaction_button.tsx b/packages/instant/src/containers/selected_asset_view_transaction_button.tsx index 6f42b9f85..064b877be 100644 --- a/packages/instant/src/containers/selected_asset_view_transaction_button.tsx +++ b/packages/instant/src/containers/selected_asset_view_transaction_button.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { State } from '../redux/reducer'; import { ViewTransactionButton } from '../components/view_transaction_button'; -import { AsyncProcessState } from '../types'; +import { OrderProcessState } from '../types'; import { etherscanUtil } from '../util/etherscan'; export interface SelectedAssetViewTransactionButtonProps {} @@ -16,9 +16,13 @@ interface ConnectedState { const mapStateToProps = (state: State, _ownProps: {}): ConnectedState => ({ onClick: () => { - if (state.assetBuyer && state.buyOrderState.processState === AsyncProcessState.SUCCESS) { + if ( + state.assetBuyer && + (state.buyOrderState.processState === OrderProcessState.PROCESSING || + state.buyOrderState.processState === OrderProcessState.SUCCESS) + ) { const etherscanUrl = etherscanUtil.getEtherScanTxnAddressIfExists( - state.buyOrderState.txnHash, + state.buyOrderState.txHash, state.assetBuyer.networkId, ); if (etherscanUrl) { diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index c6a05ac52..25d0092b2 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -4,7 +4,15 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { assetMetaDataMap } from '../data/asset_meta_data_map'; -import { Asset, AssetMetaData, AsyncProcessState, DisplayStatus, Network, OrderState } from '../types'; +import { + Asset, + AssetMetaData, + AsyncProcessState, + DisplayStatus, + Network, + OrderProcessState, + OrderState, +} from '../types'; import { assetUtils } from '../util/asset'; import { Action, ActionTypes } from './actions'; @@ -27,7 +35,7 @@ export const INITIAL_STATE: State = { network: Network.Mainnet, selectedAssetAmount: undefined, assetMetaDataMap, - buyOrderState: { processState: AsyncProcessState.NONE }, + buyOrderState: { processState: OrderProcessState.NONE }, ethUsdPrice: undefined, latestBuyQuote: undefined, latestError: undefined, @@ -106,7 +114,7 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => ...state, latestBuyQuote: undefined, quoteRequestState: AsyncProcessState.NONE, - buyOrderState: { processState: AsyncProcessState.NONE }, + buyOrderState: { processState: OrderProcessState.NONE }, selectedAssetAmount: undefined, }; default: diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index c5521c63c..c63371fb4 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -8,18 +8,22 @@ export enum AsyncProcessState { FAILURE = 'Failure', } -interface RegularOrderState { - processState: AsyncProcessState.NONE | AsyncProcessState.PENDING; +export enum OrderProcessState { + NONE = 'None', + AWAITING_SIGNATURE = 'Awaiting Signature', + PROCESSING = 'Processing', + SUCCESS = 'Success', + FAILURE = 'Failure', } -interface SuccessfulOrderState { - processState: AsyncProcessState.SUCCESS; - txnHash: string; + +interface OrderStatePreTx { + processState: OrderProcessState.NONE | OrderProcessState.AWAITING_SIGNATURE; } -interface FailureOrderState { - processState: AsyncProcessState.FAILURE; - txnHash?: string; +interface OrderStatePostTx { + processState: OrderProcessState.PROCESSING | OrderProcessState.SUCCESS | OrderProcessState.FAILURE; + txHash: string; } -export type OrderState = RegularOrderState | SuccessfulOrderState | FailureOrderState; +export type OrderState = OrderStatePreTx | OrderStatePostTx; export enum DisplayStatus { Present, diff --git a/packages/instant/src/util/error.ts b/packages/instant/src/util/error.ts index 0868414f0..844a28d8b 100644 --- a/packages/instant/src/util/error.ts +++ b/packages/instant/src/util/error.ts @@ -46,6 +46,10 @@ const humanReadableMessageForError = (error: Error, asset?: Asset): string | und return `${assetName} is currently unavailable`; } + if (error.message === AssetBuyerError.SignatureRequestDenied) { + return 'You denied this transaction'; + } + return undefined; }; diff --git a/packages/instant/src/util/etherscan.ts b/packages/instant/src/util/etherscan.ts index ffb08a382..cfc2578a3 100644 --- a/packages/instant/src/util/etherscan.ts +++ b/packages/instant/src/util/etherscan.ts @@ -14,11 +14,11 @@ const etherscanPrefix = (networkId: number): string | undefined => { }; export const etherscanUtil = { - getEtherScanTxnAddressIfExists: (txnHash: string, networkId: number) => { + getEtherScanTxnAddressIfExists: (txHash: string, networkId: number) => { const prefix = etherscanPrefix(networkId); if (_.isUndefined(prefix)) { return; } - return `https://${prefix}etherscan.io/tx/${txnHash}`; + return `https://${prefix}etherscan.io/tx/${txHash}`; }, }; diff --git a/packages/migrations/README.md b/packages/migrations/README.md index 926654cd8..b90d730eb 100644 --- a/packages/migrations/README.md +++ b/packages/migrations/README.md @@ -50,33 +50,10 @@ yarn lint ### Migrate -#### V2-beta smart contracts +#### V2 smart contracts -In order to migrate the V2-beta 0x smart contracts to Kovan using a Ledger Nano S, run: - -```bash -yarn migrate:v2-beta-testnet -``` - -**Note:** Ledger settings `contract data` must be `on`, and `browser support` must be set to `off`. - -Post-publish steps: - -1. Since we don't re-deploy the `WETH9` nor `ZRXToken` contracts, manually copy over the artifacts for them from `2.0.0` into `2.0.0-beta-testnet` and add the Kovan & ganache addresses to both of their `networks` sections. -2. We now need to copy over the network `50` settings from the `2.0.0` artifacts to the `2.0.0-beta-testnet` artifacts for the newly deployed contracts (e.g `Exchange`, `ERC20Proxy`, `ERC721Proxy` and `AssetProxyOwner`) - -#### V2 (under development) smart contracts - -In order to migrate the V2 (under development) 0x smart contracts to TestRPC/Ganache running at `http://localhost:8545`, run: +In order to migrate the V2 0x smart contracts to TestRPC/Ganache running at `http://localhost:8545`, run: ```bash yarn migrate:v2 ``` - -#### V1 smart contracts - -In order to migrate the V1 0x smart contracts to TestRPC/Ganache running at `http://localhost:8545`, run: - -```bash -yarn migrate:v1 -``` |