diff options
30 files changed, 364 insertions, 150 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 26b3e65d4..826b6150d 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "3.0.0", + "changes": [ + { + "note": "update `getBuyQuoteAsync` to return eth spent on assets instead of per unit amount", + "pr": 1252 + } + ] + }, + { "timestamp": 1542134075, "version": "2.2.2", "changes": [ diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 3f1e6ff21..3b573edca 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -54,12 +54,12 @@ export interface BuyQuote { } /** - * ethPerAssetPrice: The price of one unit of the desired asset in ETH + * assetEthAmount: The amount of eth required to pay for the requested asset. * feeEthAmount: The amount of eth required to pay the affiliate fee. - * totalEthAmount: the total amount of eth required to complete the buy. (Filling orders, feeOrders, and paying affiliate fee) + * totalEthAmount: The total amount of eth required to complete the buy (filling orders, feeOrders, and paying affiliate fee). */ export interface BuyQuoteInfo { - ethPerAssetPrice: BigNumber; + assetEthAmount: BigNumber; feeEthAmount: BigNumber; totalEthAmount: BigNumber; } diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 2466f53a4..fcf9b0d0e 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -18,7 +18,7 @@ export const assert = { } }, isValidBuyQuoteInfo(variableName: string, buyQuoteInfo: BuyQuoteInfo): void { - sharedAssert.isBigNumber(`${variableName}.ethPerAssetPrice`, buyQuoteInfo.ethPerAssetPrice); + sharedAssert.isBigNumber(`${variableName}.assetEthAmount`, buyQuoteInfo.assetEthAmount); sharedAssert.isBigNumber(`${variableName}.feeEthAmount`, buyQuoteInfo.feeEthAmount); sharedAssert.isBigNumber(`${variableName}.totalEthAmount`, buyQuoteInfo.totalEthAmount); }, diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index 6a67ed1ed..b15b880c2 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -106,28 +106,28 @@ function calculateQuoteInfo( isMakerAssetZrxToken: boolean, ): BuyQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right - let ethAmountToBuyAsset = constants.ZERO_AMOUNT; - let ethAmountToBuyZrx = constants.ZERO_AMOUNT; + let assetEthAmount = constants.ZERO_AMOUNT; + let zrxEthAmount = constants.ZERO_AMOUNT; if (isMakerAssetZrxToken) { - ethAmountToBuyAsset = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount); + assetEthAmount = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount); } else { // find eth and zrx amounts needed to buy const ethAndZrxAmountToBuyAsset = findEthAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, assetBuyAmount); - ethAmountToBuyAsset = ethAndZrxAmountToBuyAsset[0]; + assetEthAmount = ethAndZrxAmountToBuyAsset[0]; const zrxAmountToBuyAsset = ethAndZrxAmountToBuyAsset[1]; // find eth amount needed to buy zrx - ethAmountToBuyZrx = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); + zrxEthAmount = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); } - /// find the eth amount needed to buy the affiliate fee - const ethAmountToBuyAffiliateFee = ethAmountToBuyAsset.mul(feePercentage).ceil(); - 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); + // eth amount needed to buy the affiliate fee + const affiliateFeeEthAmount = assetEthAmount.mul(feePercentage).ceil(); + // eth amount needed for fees is the sum of affiliate fee and zrx fee + const feeEthAmount = affiliateFeeEthAmount.plus(zrxEthAmount); + // eth amount needed in total is the sum of the amount needed for the asset and the amount needed for fees + const totalEthAmount = assetEthAmount.plus(feeEthAmount); return { - totalEthAmount: ethAmountTotal, - feeEthAmount: ethAmountToBuyAffiliateFee, - ethPerAssetPrice, + assetEthAmount, + feeEthAmount, + totalEthAmount, }; } diff --git a/packages/asset-buyer/test/buy_quote_calculator_test.ts b/packages/asset-buyer/test/buy_quote_calculator_test.ts index 0ea371982..a30017b72 100644 --- a/packages/asset-buyer/test/buy_quote_calculator_test.ts +++ b/packages/asset-buyer/test/buy_quote_calculator_test.ts @@ -108,17 +108,17 @@ describe('buyQuoteCalculator', () => { // 50 eth to fill the first order + 100 eth for fees const expectedEthAmountForAsset = new BigNumber(50); const expectedEthAmountForZrxFees = new BigNumber(100); - const expectedFillEthAmount = expectedEthAmountForAsset.plus(expectedEthAmountForZrxFees); - const expectedFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage); + const expectedFillEthAmount = expectedEthAmountForAsset; + const expectedAffiliateFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage); + const expectedFeeEthAmount = expectedAffiliateFeeEthAmount.plus(expectedEthAmountForZrxFees); const expectedTotalEthAmount = expectedFillEthAmount.plus(expectedFeeEthAmount); - const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount); + expect(buyQuote.bestCaseQuoteInfo.assetEthAmount).to.bignumber.equal(expectedFillEthAmount); expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); - expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); // because we have no slippage protection, minRate is equal to maxRate + expect(buyQuote.worstCaseQuoteInfo.assetEthAmount).to.bignumber.equal(expectedFillEthAmount); expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); - expect(buyQuote.worstCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); // test if feePercentage gets passed through expect(buyQuote.feePercentage).to.equal(feePercentage); }); @@ -146,23 +146,23 @@ describe('buyQuoteCalculator', () => { // 50 eth to fill the first order + 100 eth for fees const expectedEthAmountForAsset = new BigNumber(50); const expectedEthAmountForZrxFees = new BigNumber(100); - const expectedFillEthAmount = expectedEthAmountForAsset.plus(expectedEthAmountForZrxFees); - const expectedFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage); + const expectedFillEthAmount = expectedEthAmountForAsset; + const expectedAffiliateFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage); + const expectedFeeEthAmount = expectedAffiliateFeeEthAmount.plus(expectedEthAmountForZrxFees); const expectedTotalEthAmount = expectedFillEthAmount.plus(expectedFeeEthAmount); - const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount); + expect(buyQuote.bestCaseQuoteInfo.assetEthAmount).to.bignumber.equal(expectedFillEthAmount); 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 + 208 eth for fees const expectedWorstEthAmountForAsset = new BigNumber(100); const expectedWorstEthAmountForZrxFees = new BigNumber(208); - const expectedWorstFillEthAmount = expectedWorstEthAmountForAsset.plus(expectedWorstEthAmountForZrxFees); - const expectedWorstFeeEthAmount = expectedWorstEthAmountForAsset.mul(feePercentage); + const expectedWorstFillEthAmount = expectedWorstEthAmountForAsset; + const expectedWorstAffiliateFeeEthAmount = expectedWorstEthAmountForAsset.mul(feePercentage); + const expectedWorstFeeEthAmount = expectedWorstAffiliateFeeEthAmount.plus(expectedWorstEthAmountForZrxFees); const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.plus(expectedWorstFeeEthAmount); - const expectedWorstEthPerAssetPrice = expectedWorstFillEthAmount.div(assetBuyAmount); + expect(buyQuote.worstCaseQuoteInfo.assetEthAmount).to.bignumber.equal(expectedWorstFillEthAmount); expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedWorstFeeEthAmount); expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedWorstTotalEthAmount); - expect(buyQuote.worstCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedWorstEthPerAssetPrice); // test if feePercentage gets passed through expect(buyQuote.feePercentage).to.equal(feePercentage); }); diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index b07776b2c..7f9567454 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -15,8 +15,8 @@ import { Spinner } from './ui/spinner'; import { Text } from './ui/text'; export interface InstantHeadingProps { - selectedAssetAmount?: BigNumber; - totalEthBaseAmount?: BigNumber; + selectedAssetUnitAmount?: BigNumber; + totalEthBaseUnitAmount?: BigNumber; ethUsdPrice?: BigNumber; quoteRequestState: AsyncProcessState; buyOrderState: OrderState; @@ -104,7 +104,7 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { if (this.props.quoteRequestState === AsyncProcessState.Pending) { return <AmountPlaceholder isPulsating={true} color={PLACEHOLDER_COLOR} />; } - if (_.isUndefined(this.props.selectedAssetAmount)) { + if (_.isUndefined(this.props.selectedAssetUnitAmount)) { return <AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />; } return amountFunction(); @@ -113,8 +113,8 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { private readonly _renderEthAmount = (): React.ReactNode => { return ( <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> - {format.ethBaseAmount( - this.props.totalEthBaseAmount, + {format.ethBaseUnitAmount( + this.props.totalEthBaseUnitAmount, 4, <AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />, )} @@ -125,8 +125,8 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { private readonly _renderDollarAmount = (): React.ReactNode => { return ( <Text fontSize="16px" fontColor={ColorOption.white}> - {format.ethBaseAmountInUsd( - this.props.totalEthBaseAmount, + {format.ethBaseUnitAmountInUsd( + this.props.totalEthBaseUnitAmount, this.props.ethUsdPrice, 2, <AmountPlaceholder isPulsating={false} color={ColorOption.white} />, diff --git a/packages/instant/src/components/order_details.tsx b/packages/instant/src/components/order_details.tsx index 9abd7137e..5fc956e1c 100644 --- a/packages/instant/src/components/order_details.tsx +++ b/packages/instant/src/components/order_details.tsx @@ -4,6 +4,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { oc } from 'ts-optchain'; +import { BIG_NUMBER_ZERO } from '../constants'; import { ColorOption } from '../style/theme'; import { format } from '../util/format'; @@ -15,16 +16,23 @@ import { Text } from './ui/text'; export interface OrderDetailsProps { buyQuoteInfo?: BuyQuoteInfo; + selectedAssetUnitAmount?: BigNumber; ethUsdPrice?: BigNumber; isLoading: boolean; } export class OrderDetails extends React.Component<OrderDetailsProps> { public render(): React.ReactNode { - const { buyQuoteInfo, ethUsdPrice } = this.props; + const { buyQuoteInfo, ethUsdPrice, selectedAssetUnitAmount } = this.props; const buyQuoteAccessor = oc(buyQuoteInfo); - const ethAssetPrice = buyQuoteAccessor.ethPerAssetPrice(); - const ethTokenFee = buyQuoteAccessor.feeEthAmount(); - const totalEthAmount = buyQuoteAccessor.totalEthAmount(); + const assetEthBaseUnitAmount = buyQuoteAccessor.assetEthAmount(); + const feeEthBaseUnitAmount = buyQuoteAccessor.feeEthAmount(); + const totalEthBaseUnitAmount = buyQuoteAccessor.totalEthAmount(); + const pricePerTokenEth = + !_.isUndefined(assetEthBaseUnitAmount) && + !_.isUndefined(selectedAssetUnitAmount) && + !selectedAssetUnitAmount.eq(BIG_NUMBER_ZERO) + ? assetEthBaseUnitAmount.div(selectedAssetUnitAmount).ceil() + : undefined; return ( <Container padding="20px" width="100%" flexGrow={1}> <Container marginBottom="10px"> @@ -40,20 +48,19 @@ export class OrderDetails extends React.Component<OrderDetailsProps> { </Container> <EthAmountRow rowLabel="Token Price" - ethAmount={ethAssetPrice} + ethAmount={pricePerTokenEth} ethUsdPrice={ethUsdPrice} - isEthAmountInBaseUnits={false} isLoading={this.props.isLoading} /> <EthAmountRow rowLabel="Fee" - ethAmount={ethTokenFee} + ethAmount={feeEthBaseUnitAmount} ethUsdPrice={ethUsdPrice} isLoading={this.props.isLoading} /> <EthAmountRow rowLabel="Total Cost" - ethAmount={totalEthAmount} + ethAmount={totalEthBaseUnitAmount} ethUsdPrice={ethUsdPrice} shouldEmphasize={true} isLoading={this.props.isLoading} @@ -81,7 +88,7 @@ export class EthAmountRow extends React.Component<EthAmountRowProps> { const { rowLabel, ethAmount, isEthAmountInBaseUnits, shouldEmphasize, isLoading } = this.props; const fontWeight = shouldEmphasize ? 700 : 400; - const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount; + const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseUnitAmount : format.ethUnitAmount; return ( <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> <Flex justify="space-between"> @@ -105,7 +112,9 @@ export class EthAmountRow extends React.Component<EthAmountRowProps> { ); } private _renderUsdSection(): React.ReactNode { - const usdFormatter = this.props.isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd; + const usdFormatter = this.props.isEthAmountInBaseUnits + ? format.ethBaseUnitAmountInUsd + : format.ethUnitAmountInUsd; const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount); return shouldHideUsdPriceSection ? null : ( <Container marginRight="3px" display="inline-block"> diff --git a/packages/instant/src/components/payment_method_dropdown.tsx b/packages/instant/src/components/payment_method_dropdown.tsx index bdce2a49d..58f1cc044 100644 --- a/packages/instant/src/components/payment_method_dropdown.tsx +++ b/packages/instant/src/components/payment_method_dropdown.tsx @@ -18,7 +18,7 @@ export class PaymentMethodDropdown extends React.Component<PaymentMethodDropdown public render(): React.ReactNode { const { accountAddress, accountEthBalanceInWei } = this.props; const value = format.ethAddress(accountAddress); - const label = format.ethBaseAmount(accountEthBalanceInWei, 4, '') as string; + const label = format.ethBaseUnitAmount(accountEthBalanceInWei, 4, '') as string; return <Dropdown value={value} label={label} items={this._getDropdownItemConfigs()} />; } private readonly _getDropdownItemConfigs = (): DropdownItemConfig[] => { diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index f6ee28dba..2267b4dbf 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; +import { ZeroExInstantContainer } from '../components/zero_ex_instant_container'; + import { INJECTED_DIV_CLASS } from '../constants'; -import { ConnectedZeroExInstantContainer } from '../containers/connected_zero_ex_instant_container'; import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; @@ -11,7 +12,7 @@ export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props return ( <div className={INJECTED_DIV_CLASS}> <ZeroExInstantProvider {...props}> - <ConnectedZeroExInstantContainer /> + <ZeroExInstantContainer /> </ZeroExInstantProvider> </div> ); diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 60f80e8d1..c0a197590 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import { AvailableERC20TokenSelector } from '../containers/available_erc20_token_selector'; -import { ConnectedAccountPaymentMethod } from '../containers/connected_account_payment_method'; +import { ConnectedBuyOrderProgressOrPaymentMethod } from '../containers/connected_buy_order_progress_or_payment_method'; import { CurrentStandardSlidingPanel } from '../containers/current_standard_sliding_panel'; import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; import { LatestError } from '../containers/latest_error'; -import { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress'; import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_buy_order_state_buttons'; import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; import { ColorOption } from '../style/theme'; @@ -24,7 +23,7 @@ export interface ZeroExInstantContainerState { tokenSelectionPanelAnimationState: SlideAnimationState; } -export class ZeroExInstantContainer extends React.Component<ZeroExInstantContainerProps, ZeroExInstantContainerState> { +export class ZeroExInstantContainer extends React.Component<{}, ZeroExInstantContainerState> { public state = { tokenSelectionPanelAnimationState: 'none' as SlideAnimationState, }; @@ -51,7 +50,7 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain > <Flex direction="column" justify="flex-start" height="100%"> <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} /> - {this._renderPaymentMethodOrBuyOrderProgress()} + <ConnectedBuyOrderProgressOrPaymentMethod /> <LatestBuyQuoteOrderDetails /> <Container padding="20px" width="100%"> <SelectedAssetBuyOrderStateButtons /> @@ -79,16 +78,4 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain tokenSelectionPanelAnimationState: 'slidOut', }); }; - private readonly _renderPaymentMethodOrBuyOrderProgress = (): React.ReactNode => { - const { orderProcessState } = this.props; - if ( - orderProcessState === OrderProcessState.Processing || - orderProcessState === OrderProcessState.Success || - orderProcessState === OrderProcessState.Failure - ) { - return <SelectedAssetBuyOrderProgress />; - } else { - return <ConnectedAccountPaymentMethod />; - } - }; } diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx index a7e1bd65a..2856ea3e3 100644 --- a/packages/instant/src/components/zero_ex_instant_overlay.tsx +++ b/packages/instant/src/components/zero_ex_instant_overlay.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { ConnectedZeroExInstantContainer } from '../containers/connected_zero_ex_instant_container'; +import { ZeroExInstantContainer } from '../components/zero_ex_instant_container'; import { ColorOption } from '../style/theme'; import { Container } from './ui/container'; @@ -31,7 +31,7 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay /> </Container> <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> - <ConnectedZeroExInstantContainer /> + <ZeroExInstantContainer /> </Container> </Flex> </Overlay> diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index 863bc99b7..18e71edb6 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -73,7 +73,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider completeAssetMetaDataMap, networkId, ), - selectedAssetAmount: _.isUndefined(props.defaultAssetBuyAmount) + selectedAssetUnitAmount: _.isUndefined(props.defaultAssetBuyAmount) ? undefined : new BigNumber(props.defaultAssetBuyAmount), availableAssets: _.isUndefined(props.availableAssetDatas) diff --git a/packages/instant/src/containers/connected_buy_order_progress_or_payment_method.tsx b/packages/instant/src/containers/connected_buy_order_progress_or_payment_method.tsx new file mode 100644 index 000000000..05071c8c3 --- /dev/null +++ b/packages/instant/src/containers/connected_buy_order_progress_or_payment_method.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { OrderProcessState } from '../types'; + +import { ConnectedAccountPaymentMethod } from './connected_account_payment_method'; +import { SelectedAssetBuyOrderProgress } from './selected_asset_buy_order_progress'; + +interface BuyOrderProgressOrPaymentMethodProps { + orderProcessState: OrderProcessState; +} +export const BuyOrderProgressOrPaymentMethod = (props: BuyOrderProgressOrPaymentMethodProps) => { + const { orderProcessState } = props; + if ( + orderProcessState === OrderProcessState.Processing || + orderProcessState === OrderProcessState.Success || + orderProcessState === OrderProcessState.Failure + ) { + return <SelectedAssetBuyOrderProgress />; + } + if (orderProcessState === OrderProcessState.None) { + return <ConnectedAccountPaymentMethod />; + } + return null; +}; + +interface ConnectedState extends BuyOrderProgressOrPaymentMethodProps {} + +export interface ConnectedBuyOrderProgressOrPaymentMethodProps {} +const mapStateToProps = (state: State, _ownProps: ConnectedBuyOrderProgressOrPaymentMethodProps): ConnectedState => ({ + orderProcessState: state.buyOrderState.processState, +}); +export const ConnectedBuyOrderProgressOrPaymentMethod: React.ComponentClass< + ConnectedBuyOrderProgressOrPaymentMethodProps +> = connect(mapStateToProps)(BuyOrderProgressOrPaymentMethod); diff --git a/packages/instant/src/containers/connected_zero_ex_instant_container.ts b/packages/instant/src/containers/connected_zero_ex_instant_container.ts deleted file mode 100644 index 9606d18c2..000000000 --- a/packages/instant/src/containers/connected_zero_ex_instant_container.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; - -import { State } from '../redux/reducer'; -import { OrderProcessState } from '../types'; - -import { ZeroExInstantContainer } from '../components/zero_ex_instant_container'; - -export interface ConnectedZeroExInstantContainerProps {} - -interface ConnectedState { - orderProcessState: OrderProcessState; -} - -const mapStateToProps = (state: State, _ownProps: ConnectedZeroExInstantContainerProps): ConnectedState => ({ - orderProcessState: state.buyOrderState.processState, -}); - -export const ConnectedZeroExInstantContainer: React.ComponentClass<ConnectedZeroExInstantContainerProps> = connect( - mapStateToProps, -)(ZeroExInstantContainer); diff --git a/packages/instant/src/containers/latest_buy_quote_order_details.ts b/packages/instant/src/containers/latest_buy_quote_order_details.ts index 2b59ed3ae..5dfe535e7 100644 --- a/packages/instant/src/containers/latest_buy_quote_order_details.ts +++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts @@ -14,6 +14,7 @@ export interface LatestBuyQuoteOrderDetailsProps {} interface ConnectedState { buyQuoteInfo?: BuyQuoteInfo; + selectedAssetUnitAmount?: BigNumber; ethUsdPrice?: BigNumber; isLoading: boolean; } @@ -21,6 +22,7 @@ interface ConnectedState { const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({ // use the worst case quote info buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(), + selectedAssetUnitAmount: state.selectedAssetUnitAmount, ethUsdPrice: state.ethUsdPrice, isLoading: state.quoteRequestState === AsyncProcessState.Pending, }); diff --git a/packages/instant/src/containers/selected_asset_instant_heading.ts b/packages/instant/src/containers/selected_asset_instant_heading.ts index a407279e6..8dc127e1d 100644 --- a/packages/instant/src/containers/selected_asset_instant_heading.ts +++ b/packages/instant/src/containers/selected_asset_instant_heading.ts @@ -14,16 +14,16 @@ export interface InstantHeadingProps { } interface ConnectedState { - selectedAssetAmount?: BigNumber; - totalEthBaseAmount?: BigNumber; + selectedAssetUnitAmount?: BigNumber; + totalEthBaseUnitAmount?: BigNumber; ethUsdPrice?: BigNumber; quoteRequestState: AsyncProcessState; buyOrderState: OrderState; } const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({ - selectedAssetAmount: state.selectedAssetAmount, - totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(), + selectedAssetUnitAmount: state.selectedAssetUnitAmount, + totalEthBaseUnitAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(), ethUsdPrice: state.ethUsdPrice, quoteRequestState: state.quoteRequestState, buyOrderState: state.buyOrderState, diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index 30eb900e6..8b0070228 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -53,7 +53,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP const assetBuyer = state.providerState.assetBuyer; return { assetBuyer, - value: state.selectedAssetAmount, + value: state.selectedAssetUnitAmount, asset: selectedAsset, isDisabled, numberOfAssetsAvailable, diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index 0891170b4..77e3dec12 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -26,7 +26,7 @@ export enum ActionTypes { SET_ACCOUNT_STATE_READY = 'SET_ACCOUNT_STATE_READY', UPDATE_ACCOUNT_ETH_BALANCE = 'UPDATE_ACCOUNT_ETH_BALANCE', UPDATE_ETH_USD_PRICE = 'UPDATE_ETH_USD_PRICE', - UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT', + UPDATE_SELECTED_ASSET_UNIT_AMOUNT = 'UPDATE_SELECTED_ASSET_UNIT_AMOUNT', SET_BUY_ORDER_STATE_NONE = 'SET_BUY_ORDER_STATE_NONE', SET_BUY_ORDER_STATE_VALIDATING = 'SET_BUY_ORDER_STATE_VALIDATING', SET_BUY_ORDER_STATE_PROCESSING = 'SET_BUY_ORDER_STATE_PROCESSING', @@ -52,7 +52,8 @@ export const actions = { updateAccountEthBalance: (addressAndBalance: AddressAndEthBalanceInWei) => createAction(ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE, addressAndBalance), updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), - updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), + updateSelectedAssetAmount: (amount?: BigNumber) => + createAction(ActionTypes.UPDATE_SELECTED_ASSET_UNIT_AMOUNT, amount), setBuyOrderStateNone: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_NONE), setBuyOrderStateValidating: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_VALIDATING), setBuyOrderStateProcessing: (txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 15ac31a5a..a1952e429 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -84,10 +84,10 @@ export const asyncData = { dispatch: Dispatch, shouldSetPending: boolean = false, ) => { - const { buyOrderState, providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = state; + const { buyOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state; const assetBuyer = providerState.assetBuyer; if ( - !_.isUndefined(selectedAssetAmount) && + !_.isUndefined(selectedAssetUnitAmount) && !_.isUndefined(selectedAsset) && buyOrderState.processState === OrderProcessState.None && selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 @@ -96,7 +96,7 @@ export const asyncData = { assetBuyer, dispatch, selectedAsset as ERC20Asset, - selectedAssetAmount, + selectedAssetUnitAmount, shouldSetPending, affiliateInfo, ); diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 3d7c3f483..dfc2b89f3 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -44,7 +44,7 @@ interface PropsDerivedState { interface OptionalState { selectedAsset: Asset; availableAssets: Asset[]; - selectedAssetAmount: BigNumber; + selectedAssetUnitAmount: BigNumber; ethUsdPrice: BigNumber; latestBuyQuote: BuyQuote; latestErrorMessage: string; @@ -105,10 +105,10 @@ export const createReducer = (initialState: State) => { ...state, ethUsdPrice: action.data, }; - case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT: + case ActionTypes.UPDATE_SELECTED_ASSET_UNIT_AMOUNT: return { ...state, - selectedAssetAmount: action.data, + selectedAssetUnitAmount: action.data, }; case ActionTypes.UPDATE_LATEST_BUY_QUOTE: const newBuyQuoteIfExists = action.data; @@ -219,7 +219,7 @@ export const createReducer = (initialState: State) => { latestBuyQuote: undefined, quoteRequestState: AsyncProcessState.None, buyOrderState: { processState: OrderProcessState.None }, - selectedAssetAmount: undefined, + selectedAssetUnitAmount: undefined, }; case ActionTypes.SET_AVAILABLE_ASSETS: return { @@ -263,9 +263,9 @@ const reduceStateWithAccount = (state: State, account: Account) => { const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => { const selectedAssetIfExists = state.selectedAsset; - const selectedAssetAmountIfExists = state.selectedAssetAmount; + const selectedAssetUnitAmountIfExists = state.selectedAssetUnitAmount; // if no selectedAsset or selectedAssetAmount exists on the current state, return false - if (_.isUndefined(selectedAssetIfExists) || _.isUndefined(selectedAssetAmountIfExists)) { + if (_.isUndefined(selectedAssetIfExists) || _.isUndefined(selectedAssetUnitAmountIfExists)) { return false; } // if buyQuote's assetData does not match that of the current selected asset, return false @@ -277,7 +277,7 @@ const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => { const selectedAssetMetaData = selectedAssetIfExists.metaData; if (selectedAssetMetaData.assetProxyId === AssetProxyId.ERC20) { const selectedAssetAmountBaseUnits = Web3Wrapper.toBaseUnitAmount( - selectedAssetAmountIfExists, + selectedAssetUnitAmountIfExists, selectedAssetMetaData.decimals, ); const doesAssetAmountMatch = selectedAssetAmountBaseUnits.eq(buyQuote.assetBuyAmount); diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts index c33e28f1c..fcdded0a9 100644 --- a/packages/instant/src/util/buy_quote_updater.ts +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -15,12 +15,12 @@ export const buyQuoteUpdater = { assetBuyer: AssetBuyer, dispatch: Dispatch<Action>, asset: ERC20Asset, - assetAmount: BigNumber, + assetUnitAmount: BigNumber, setPending = true, affiliateInfo?: AffiliateInfo, ): Promise<void> => { // get a new buy quote. - const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); + const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals); if (setPending) { // mark quote as pending dispatch(actions.setQuoteRequestStatePending()); diff --git a/packages/instant/src/util/format.ts b/packages/instant/src/util/format.ts index 44661d697..e9c432b2f 100644 --- a/packages/instant/src/util/format.ts +++ b/packages/instant/src/util/format.ts @@ -5,15 +5,15 @@ import * as _ from 'lodash'; import { ETH_DECIMALS } from '../constants'; export const format = { - ethBaseAmount: ( - ethBaseAmount?: BigNumber, + ethBaseUnitAmount: ( + ethBaseUnitAmount?: BigNumber, decimalPlaces: number = 4, defaultText: React.ReactNode = '0 ETH', ): React.ReactNode => { - if (_.isUndefined(ethBaseAmount)) { + if (_.isUndefined(ethBaseUnitAmount)) { return defaultText; } - const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ETH_DECIMALS); + const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseUnitAmount, ETH_DECIMALS); return format.ethUnitAmount(ethUnitAmount, decimalPlaces); }, ethUnitAmount: ( @@ -27,16 +27,16 @@ export const format = { const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces); return `${roundedAmount} ETH`; }, - ethBaseAmountInUsd: ( - ethBaseAmount?: BigNumber, + ethBaseUnitAmountInUsd: ( + ethBaseUnitAmount?: BigNumber, ethUsdPrice?: BigNumber, decimalPlaces: number = 2, defaultText: React.ReactNode = '$0.00', ): React.ReactNode => { - if (_.isUndefined(ethBaseAmount) || _.isUndefined(ethUsdPrice)) { + if (_.isUndefined(ethBaseUnitAmount) || _.isUndefined(ethUsdPrice)) { return defaultText; } - const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ETH_DECIMALS); + const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseUnitAmount, ETH_DECIMALS); return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces); }, ethUnitAmountInUsd: ( diff --git a/packages/instant/test/util/format.test.ts b/packages/instant/test/util/format.test.ts index c346b7604..fe0a63e6e 100644 --- a/packages/instant/test/util/format.test.ts +++ b/packages/instant/test/util/format.test.ts @@ -15,20 +15,20 @@ const BIG_NUMBER_FAKE_ETH_USD_PRICE = new BigNumber(2.534); describe('format', () => { describe('ethBaseAmount', () => { it('converts 1 ETH in base units to the string `1 ETH`', () => { - expect(format.ethBaseAmount(ONE_ETH_IN_BASE_UNITS)).toBe('1 ETH'); + expect(format.ethBaseUnitAmount(ONE_ETH_IN_BASE_UNITS)).toBe('1 ETH'); }); it('converts .432414 ETH in base units to the string `.4324 ETH`', () => { - expect(format.ethBaseAmount(DECIMAL_ETH_IN_BASE_UNITS)).toBe('0.4324 ETH'); + expect(format.ethBaseUnitAmount(DECIMAL_ETH_IN_BASE_UNITS)).toBe('0.4324 ETH'); }); it('converts 5.3014059295032 ETH in base units to the string `5.301 ETH`', () => { - expect(format.ethBaseAmount(IRRATIONAL_ETH_IN_BASE_UNITS)).toBe('5.301 ETH'); + expect(format.ethBaseUnitAmount(IRRATIONAL_ETH_IN_BASE_UNITS)).toBe('5.301 ETH'); }); it('returns defaultText param when ethBaseAmount is not defined', () => { const defaultText = 'defaultText'; - expect(format.ethBaseAmount(undefined, 4, defaultText)).toBe(defaultText); + expect(format.ethBaseUnitAmount(undefined, 4, defaultText)).toBe(defaultText); }); it('it allows for configurable decimal places', () => { - expect(format.ethBaseAmount(DECIMAL_ETH_IN_BASE_UNITS, 2)).toBe('0.43 ETH'); + expect(format.ethBaseUnitAmount(DECIMAL_ETH_IN_BASE_UNITS, 2)).toBe('0.43 ETH'); }); }); describe('ethUnitAmount', () => { @@ -52,24 +52,26 @@ describe('format', () => { }); describe('ethBaseAmountInUsd', () => { it('correctly formats 1 ETH to usd according to some price', () => { - expect(format.ethBaseAmountInUsd(ONE_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('$2.53'); + expect(format.ethBaseUnitAmountInUsd(ONE_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('$2.53'); }); it('correctly formats .432414 ETH to usd according to some price', () => { - expect(format.ethBaseAmountInUsd(DECIMAL_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('$1.10'); + expect(format.ethBaseUnitAmountInUsd(DECIMAL_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe( + '$1.10', + ); }); it('correctly formats 5.3014059295032 ETH to usd according to some price', () => { - expect(format.ethBaseAmountInUsd(IRRATIONAL_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe( + expect(format.ethBaseUnitAmountInUsd(IRRATIONAL_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe( '$13.43', ); }); it('returns defaultText param when ethBaseAmountInUsd or ethUsdPrice is not defined', () => { const defaultText = 'defaultText'; - expect(format.ethBaseAmountInUsd(undefined, undefined, 2, defaultText)).toBe(defaultText); - expect(format.ethBaseAmountInUsd(BIG_NUMBER_ONE, undefined, 2, defaultText)).toBe(defaultText); - expect(format.ethBaseAmountInUsd(undefined, BIG_NUMBER_ONE, 2, defaultText)).toBe(defaultText); + expect(format.ethBaseUnitAmountInUsd(undefined, undefined, 2, defaultText)).toBe(defaultText); + expect(format.ethBaseUnitAmountInUsd(BIG_NUMBER_ONE, undefined, 2, defaultText)).toBe(defaultText); + expect(format.ethBaseUnitAmountInUsd(undefined, BIG_NUMBER_ONE, 2, defaultText)).toBe(defaultText); }); it('it allows for configurable decimal places', () => { - expect(format.ethBaseAmountInUsd(DECIMAL_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE, 4)).toBe( + expect(format.ethBaseUnitAmountInUsd(DECIMAL_ETH_IN_BASE_UNITS, BIG_NUMBER_FAKE_ETH_USD_PRICE, 4)).toBe( '$1.0957', ); }); diff --git a/packages/utils/test/sign_typed_data_utils_test.ts b/packages/utils/test/sign_typed_data_utils_test.ts index dcba08b04..3d2cb2496 100644 --- a/packages/utils/test/sign_typed_data_utils_test.ts +++ b/packages/utils/test/sign_typed_data_utils_test.ts @@ -136,5 +136,28 @@ describe('signTypedDataUtils', () => { const hashHex = `0x${hash}`; expect(hashHex).to.be.eq(orderSignTypedDataHashHex); }); + it('creates a hash of an uninitialized order', () => { + const uninitializedOrder = { + ...orderSignTypedData, + message: { + makerAddress: '0x0000000000000000000000000000000000000000', + takerAddress: '0x0000000000000000000000000000000000000000', + makerAssetAmount: 0, + takerAssetAmount: 0, + expirationTimeSeconds: 0, + makerFee: 0, + takerFee: 0, + feeRecipientAddress: '0x0000000000000000000000000000000000000000', + senderAddress: '0x0000000000000000000000000000000000000000', + salt: 0, + makerAssetData: '0x0000000000000000000000000000000000000000', + takerAssetData: '0x0000000000000000000000000000000000000000', + exchangeAddress: '0x0000000000000000000000000000000000000000', + }, + }; + const hash = signTypedDataUtils.generateTypedDataHash(uninitializedOrder).toString('hex'); + const hashHex = `0x${hash}`; + expect(hashHex).to.be.eq('0xfaa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422'); + }); }); }); diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index 1b07b612c..7f1da2f34 100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -160,7 +160,13 @@ setup( "publish": PublishCommand, "ganache": GanacheCommand, }, - install_requires=["eth-abi", "eth_utils", "mypy_extensions", "web3"], + install_requires=[ + "eth-abi", + "eth_utils", + "ethereum", + "mypy_extensions", + "web3", + ], extras_require={ "dev": [ "bandit", diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py index 71b6128ca..9afeacfdf 100644 --- a/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py +++ b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py @@ -10,8 +10,8 @@ from typing import Any, List from mypy_extensions import TypedDict -from eth_abi import encode_abi from web3 import Web3 +from eth_abi import encode_abi from .type_assertions import assert_is_string, assert_is_list diff --git a/python-packages/order_utils/src/zero_ex/order_utils/__init__.py b/python-packages/order_utils/src/zero_ex/order_utils/__init__.py index 80445cb6e..fb5bc2f5d 100644 --- a/python-packages/order_utils/src/zero_ex/order_utils/__init__.py +++ b/python-packages/order_utils/src/zero_ex/order_utils/__init__.py @@ -9,3 +9,157 @@ just this purpose. To start it: ``docker run -d -p 8545:8545 0xorg/ganache-cli --networkId 50 -m "concert load couple harbor equip island argue ramp clarify fence smart topic"``. """ + +import json +from typing import Dict +from pkg_resources import resource_string + +from mypy_extensions import TypedDict + +from eth_utils import is_address, keccak, to_checksum_address, to_bytes +from web3 import Web3 +from web3.utils import datatypes +import web3.exceptions + + +class Constants: # pylint: disable=too-few-public-methods + """Static data used by order utilities.""" + + contract_name_to_abi = { + "Exchange": json.loads( + resource_string( + "zero_ex.contract_artifacts", "artifacts/Exchange.json" + ) + )["compilerOutput"]["abi"] + } + + network_to_exchange_addr: Dict[str, str] = { + "1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", + "3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf", + "42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2", + "50": "0x48bacb9266a570d521063ef5dd96e61686dbe788", + } + + null_address = "0x0000000000000000000000000000000000000000" + + eip191_header = b"\x19\x01" + + eip712_domain_separator_schema_hash = keccak( + b"EIP712Domain(string name,string version,address verifyingContract)" + ) + + eip712_domain_struct_header = ( + eip712_domain_separator_schema_hash + + keccak(b"0x Protocol") + + keccak(b"2") + ) + + eip712_order_schema_hash = keccak( + b"Order(" + + b"address makerAddress," + + b"address takerAddress," + + b"address feeRecipientAddress," + + b"address senderAddress," + + b"uint256 makerAssetAmount," + + b"uint256 takerAssetAmount," + + b"uint256 makerFee," + + b"uint256 takerFee," + + b"uint256 expirationTimeSeconds," + + b"uint256 salt," + + b"bytes makerAssetData," + + b"bytes takerAssetData" + + b")" + ) + + +class Order(TypedDict): # pylint: disable=too-many-instance-attributes + """Object representation of a 0x order.""" + + maker_address: str + taker_address: str + fee_recipient_address: str + sender_address: str + maker_asset_amount: int + taker_asset_amount: int + maker_fee: int + taker_fee: int + expiration_time_seconds: int + salt: int + maker_asset_data: str + taker_asset_data: str + + +def make_empty_order() -> Order: + """Construct an empty order.""" + return { + "maker_address": Constants.null_address, + "taker_address": Constants.null_address, + "sender_address": Constants.null_address, + "fee_recipient_address": Constants.null_address, + "maker_asset_data": Constants.null_address, + "taker_asset_data": Constants.null_address, + "salt": 0, + "maker_fee": 0, + "taker_fee": 0, + "maker_asset_amount": 0, + "taker_asset_amount": 0, + "expiration_time_seconds": 0, + } + + +def generate_order_hash_hex(order: Order, exchange_address: str) -> str: + # docstring considered all one line by pylint: disable=line-too-long + """Calculate the hash of the given order as a hexadecimal string. + + >>> generate_order_hash_hex( + ... { + ... 'maker_address': "0x0000000000000000000000000000000000000000", + ... 'taker_address': "0x0000000000000000000000000000000000000000", + ... 'fee_recipient_address': "0x0000000000000000000000000000000000000000", + ... 'sender_address': "0x0000000000000000000000000000000000000000", + ... 'maker_asset_amount': 1000000000000000000, + ... 'taker_asset_amount': 1000000000000000000, + ... 'maker_fee': 0, + ... 'taker_fee': 0, + ... 'expiration_time_seconds': 12345, + ... 'salt': 12345, + ... 'maker_asset_data': "0000000000000000000000000000000000000000", + ... 'taker_asset_data': "0000000000000000000000000000000000000000", + ... }, + ... exchange_address="0x0000000000000000000000000000000000000000", + ... ) + '55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692' + """ # noqa: E501 (line too long) + # TODO: use JSON schema validation to validate order. pylint: disable=fixme + def pad_20_bytes_to_32(twenty_bytes: bytes): + return bytes(12) + twenty_bytes + + def int_to_32_big_endian_bytes(i: int): + return i.to_bytes(32, byteorder="big") + + eip712_domain_struct_hash = keccak( + Constants.eip712_domain_struct_header + + pad_20_bytes_to_32(to_bytes(hexstr=exchange_address)) + ) + + eip712_order_struct_hash = keccak( + Constants.eip712_order_schema_hash + + pad_20_bytes_to_32(to_bytes(hexstr=order["maker_address"])) + + pad_20_bytes_to_32(to_bytes(hexstr=order["taker_address"])) + + pad_20_bytes_to_32(to_bytes(hexstr=order["fee_recipient_address"])) + + pad_20_bytes_to_32(to_bytes(hexstr=order["sender_address"])) + + int_to_32_big_endian_bytes(order["maker_asset_amount"]) + + int_to_32_big_endian_bytes(order["taker_asset_amount"]) + + int_to_32_big_endian_bytes(order["maker_fee"]) + + int_to_32_big_endian_bytes(order["taker_fee"]) + + int_to_32_big_endian_bytes(order["expiration_time_seconds"]) + + int_to_32_big_endian_bytes(order["salt"]) + + keccak(to_bytes(hexstr=order["maker_asset_data"])) + + keccak(to_bytes(hexstr=order["taker_asset_data"])) + ) + + return keccak( + Constants.eip191_header + + eip712_domain_struct_hash + + eip712_order_struct_hash + ).hex() diff --git a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py index 12525ba88..2e75be6d5 100644 --- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py +++ b/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py @@ -1,31 +1,17 @@ """Signature utilities.""" -from typing import Dict, Tuple -import json -from pkg_resources import resource_string +from typing import Tuple from eth_utils import is_address, to_checksum_address from web3 import Web3 import web3.exceptions from web3.utils import datatypes +from zero_ex.order_utils import Constants from zero_ex.dev_utils.type_assertions import assert_is_hex_string # prefer `black` formatting. pylint: disable=C0330 -EXCHANGE_ABI = json.loads( - resource_string("zero_ex.contract_artifacts", "artifacts/Exchange.json") -)["compilerOutput"]["abi"] - -network_to_exchange_addr: Dict[str, str] = { - "1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", - "3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf", - "42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2", - "50": "0x48bacb9266a570d521063ef5dd96e61686dbe788", -} - - -# prefer `black` formatting. pylint: disable=C0330 def is_valid_signature( provider: Web3.HTTPProvider, data: str, signature: str, signer_address: str ) -> Tuple[bool, str]: @@ -63,10 +49,11 @@ def is_valid_signature( web3_instance = Web3(provider) # false positive from pylint: disable=no-member network_id = web3_instance.net.version - contract_address = network_to_exchange_addr[network_id] + contract_address = Constants.network_to_exchange_addr[network_id] # false positive from pylint: disable=no-member contract: datatypes.Contract = web3_instance.eth.contract( - address=to_checksum_address(contract_address), abi=EXCHANGE_ABI + address=to_checksum_address(contract_address), + abi=Constants.contract_name_to_abi["Exchange"], ) try: return ( diff --git a/python-packages/order_utils/stubs/sha3/__init__.pyi b/python-packages/order_utils/stubs/sha3/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/sha3/__init__.pyi diff --git a/python-packages/order_utils/test/test_generate_order_hash_hex.py b/python-packages/order_utils/test/test_generate_order_hash_hex.py new file mode 100644 index 000000000..e393f38d7 --- /dev/null +++ b/python-packages/order_utils/test/test_generate_order_hash_hex.py @@ -0,0 +1,18 @@ +"""Test zero_ex.order_utils.get_order_hash_hex().""" + +from zero_ex.order_utils import ( + generate_order_hash_hex, + make_empty_order, + Constants, +) + + +def test_get_order_hash_hex__empty_order(): + """Test the hashing of an uninitialized order.""" + expected_hash_hex = ( + "faa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422" + ) + actual_hash_hex = generate_order_hash_hex( + make_empty_order(), Constants.null_address + ) + assert actual_hash_hex == expected_hash_hex |