diff --git a/.env.example b/.env.example index 50e878212..2126f0ada 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,4 @@ REACT_APP_LOGGER_ID='0x-launch-kit-frontend' REACT_APP_THEME_NAME='DEFAULT_THEME' REACT_APP_ENABLE_NO_METAMASK_PROMPT='true' REACT_APP_COLLECTIBLES_SOURCE='opensea' -REACT_APP_COLLECTIBLE_NAME='Cryptokitties' +REACT_APP_COLLECTIBLE_NAME='CryptoKitties' diff --git a/package.json b/package.json index 324a80a99..89084fb84 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "0x.js": "^6.0.8", + "0x.js": "^6.0.9", "@0x/connect": "^5.0.8", "@0x/web3-wrapper": "^6.0.6", "connected-react-router": "^6.2.2", diff --git a/src/components/erc721/collectibles/collectibles_card_list.tsx b/src/components/erc721/collectibles/collectibles_card_list.tsx index b951bd597..03fca9aa1 100644 --- a/src/components/erc721/collectibles/collectibles_card_list.tsx +++ b/src/components/erc721/collectibles/collectibles_card_list.tsx @@ -2,6 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import { themeBreakPoints } from '../../../themes/commons'; +import { getCollectiblePrice } from '../../../util/collectibles'; import { Collectible } from '../../../util/types'; import { CollectibleAssetContainer } from './collectible_details'; @@ -43,8 +44,8 @@ export const CollectiblesCardList = (props: Props) => { <CollectiblesListOverflow> <CollectiblesList> {collectibles.map((item, index) => { - const { name, image, color, order, tokenId } = item; - const price = order ? order.takerAssetAmount : null; + const { name, image, color, tokenId } = item; + const price = getCollectiblePrice(item); return ( <CollectibleAssetContainer color={color} diff --git a/src/components/erc721/marketplace/collectible_buy_sell.tsx b/src/components/erc721/marketplace/collectible_buy_sell.tsx index bf89b1343..f2d231777 100644 --- a/src/components/erc721/marketplace/collectible_buy_sell.tsx +++ b/src/components/erc721/marketplace/collectible_buy_sell.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; +import { ETH_DECIMALS } from '../../../common/constants'; import { cancelOrderCollectible, selectCollectible } from '../../../store/collectibles/actions'; import { getCollectibleById, getEthAccount } from '../../../store/selectors'; import { startBuyCollectibleSteps } from '../../../store/ui/actions'; import { themeDimensions } from '../../../themes/commons'; +import { getCollectiblePrice } from '../../../util/collectibles'; import { getEndDateStringFromTimeInSeconds } from '../../../util/time_utils'; +import { tokenAmountInUnits } from '../../../util/tokens'; import { Collectible, StoreState } from '../../../util/types'; import { TradeButton } from './trade_button'; @@ -106,9 +109,7 @@ class CollectibleBuySell extends React.Component<Props> { return null; } const { color, image, order } = collectible; - - const price = order ? order.takerAssetAmount : null; - + const price = getCollectiblePrice(collectible); const expDate = order && order.expirationTimeSeconds ? getEndDateStringFromTimeInSeconds(order.expirationTimeSeconds) @@ -127,10 +128,14 @@ class CollectibleBuySell extends React.Component<Props> { /> {expDate ? ( <CollectibleText> - {timeSVG()} {expDate} + {timeSVG()} Ends {expDate} </CollectibleText> ) : null} - {price && <CollectibleText textAlign="center">Last price: Ξ {price.toString()}</CollectibleText>} + {price && ( + <CollectibleText textAlign="center"> + Last price: Ξ {tokenAmountInUnits(price, ETH_DECIMALS)} + </CollectibleText> + )} </BuySellWrapper> ); }; diff --git a/src/components/erc721/marketplace/collectible_description.tsx b/src/components/erc721/marketplace/collectible_description.tsx index 874069aa8..365a66772 100644 --- a/src/components/erc721/marketplace/collectible_description.tsx +++ b/src/components/erc721/marketplace/collectible_description.tsx @@ -2,15 +2,16 @@ import React from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; +import { COLLECTIBLE_NAME } from '../../../common/constants'; import { getCollectibleById, getEthAccount } from '../../../store/selectors'; -import { themeBreakPoints } from '../../../themes/commons'; import { truncateAddress } from '../../../util/number_utils'; -import { convertTimeInSecondsToDaysAndHours } from '../../../util/time_utils'; import { Collectible, StoreState } from '../../../util/types'; import { Card } from '../../common/card'; import { OutsideUrlIcon } from '../../common/icons/outside_url_icon'; import { CustomTD, Table, TBody, TR } from '../../common/table'; +import { DutchAuctionPriceChartCard } from './dutch_auction_price_chart_card'; + const CollectibleDescriptionWrapper = styled.div``; const CustomTDStyled = styled(CustomTD)` @@ -57,7 +58,7 @@ const CollectibleDescriptionTypeImage = styled.span<{ backgroundImage: string }> width: 16px; `; -const CollectibleDescriptionInnerTitle = styled.h4` +export const CollectibleDescriptionInnerTitle = styled.h4` color: ${props => props.theme.componentsTheme.cardTitleColor}; font-size: 14px; font-weight: 500; @@ -99,72 +100,6 @@ const CollectibleOwnerText = styled.p` margin: 0; `; -const PriceChartContainer = styled.div` - display: flex; - justify-content: space-between; - flex-direction: column; - - @media (min-width: ${themeBreakPoints.xl}) { - flex-direction: row; - } -`; - -const PriceChartPriceAndTime = styled.div` - margin-bottom: 25px; - max-width: 150px; - padding-right: 15px; - padding-top: 25px; - - @media (min-width: ${themeBreakPoints.xl}) { - margin-bottom: 0; - } -`; - -const PriceChartTitle = styled.h5` - color: ${props => props.theme.componentsTheme.cardTitleColor}; - font-size: 14px; - font-weight: 400; - line-height: 1.2; - margin: 0 0 6px; -`; - -const PriceChartValue = styled.p` - color: #00ae99; - font-size: 14px; - line-height: 1.2; - margin: 0 0 35px; - - &:last-child { - margin-bottom: 0; - } -`; - -const PriceChartValueNeutral = styled(PriceChartValue)` - color: ${props => props.theme.componentsTheme.cardTitleColor}; -`; - -const PriceChartGraphWrapper = styled.div` - flex-grow: 1; - padding-bottom: 15px; - - @media (min-width: ${themeBreakPoints.xl}) { - max-width: 365px; - } -`; - -const PriceChartGraph = styled.div` - background-color: #f5f5f5; - height: 148px; - margin: 0 0 15px; - width: 100%; -`; - -const PriceChartGraphValues = styled.div` - align-items: center; - display: flex; - justify-content: space-between; -`; - const TransactionContainerTableWrapper = styled.div` overflow-x: auto; width: 100%; @@ -203,20 +138,11 @@ const CollectibleDescription = (props: Props) => { return null; } - const { currentOwner, description, order, name, assetUrl } = collectible; - const emptyPlaceholder = '----'; - const price = order ? order.takerAssetAmount : null; + const { currentOwner, description, name, assetUrl } = collectible; const tableTitlesStyling = { fontWeight: '500', color: '#0036f4' }; const typeImage = 'https://placeimg.com/32/32/any'; const ownerImage = 'https://placeimg.com/50/50/any'; - let timeRemaining = ''; - - if (order && order.expirationTimeSeconds) { - const daysAndHours = convertTimeInSecondsToDaysAndHours(order.expirationTimeSeconds); - timeRemaining = `${daysAndHours.days} Days ${daysAndHours.hours} Hrs`; - } - const doesBelongToCurrentUser = currentOwner.toLowerCase() === ethAccount.toLowerCase(); return ( @@ -226,7 +152,7 @@ const CollectibleDescription = (props: Props) => { <CollectibleDescriptionTitle>{name}</CollectibleDescriptionTitle> <CollectibleDescriptionType href={assetUrl} target="_blank"> <CollectibleDescriptionTypeImage backgroundImage={typeImage} /> - <CollectibleDescriptionTypeText>CryptoKitties</CollectibleDescriptionTypeText> + <CollectibleDescriptionTypeText>{COLLECTIBLE_NAME}</CollectibleDescriptionTypeText> {OutsideUrlIcon()} </CollectibleDescriptionType> </CollectibleDescriptionTitleWrapper> @@ -242,41 +168,14 @@ const CollectibleDescription = (props: Props) => { <CollectibleOwnerWrapper> <CollectibleOwnerImage backgroundImage={ownerImage} /> <CollectibleOwnerText> - ${truncateAddress(currentOwner)} + {truncateAddress(currentOwner)} {doesBelongToCurrentUser && ' (you)'} </CollectibleOwnerText> </CollectibleOwnerWrapper> </> ) : null} </Card> - <Card> - <CollectibleDescriptionInnerTitle>Price Chart</CollectibleDescriptionInnerTitle> - <PriceChartContainer> - <PriceChartPriceAndTime> - <PriceChartTitle>Current Price</PriceChartTitle> - <PriceChartValue> - {price ? <span>{price.toString()} ETH</span> : emptyPlaceholder} - </PriceChartValue> - <PriceChartTitle>Time Remaining</PriceChartTitle> - <PriceChartValue> - {timeRemaining ? <span>{timeRemaining}</span> : emptyPlaceholder} - </PriceChartValue> - </PriceChartPriceAndTime> - <PriceChartGraphWrapper> - <PriceChartGraph /> - <PriceChartGraphValues> - <div> - <PriceChartTitle>Start Price</PriceChartTitle> - <PriceChartValueNeutral>5.00 ETH</PriceChartValueNeutral> - </div> - <div> - <PriceChartTitle>End Price</PriceChartTitle> - <PriceChartValueNeutral>3.00 ETH</PriceChartValueNeutral> - </div> - </PriceChartGraphValues> - </PriceChartGraphWrapper> - </PriceChartContainer> - </Card> + <DutchAuctionPriceChartCard collectible={collectible} /> <Card> <CollectibleDescriptionInnerTitle>Transaction history</CollectibleDescriptionInnerTitle> <TransactionContainerTableWrapper> diff --git a/src/components/erc721/marketplace/dutch_auction_price_chart_card.tsx b/src/components/erc721/marketplace/dutch_auction_price_chart_card.tsx new file mode 100644 index 000000000..2c45c43c5 --- /dev/null +++ b/src/components/erc721/marketplace/dutch_auction_price_chart_card.tsx @@ -0,0 +1,127 @@ +import { BigNumber } from '0x.js'; +import React from 'react'; +import styled from 'styled-components'; + +import { ETH_DECIMALS } from '../../../common/constants'; +import { themeBreakPoints } from '../../../themes/commons'; +import { getCollectiblePrice } from '../../../util/collectibles'; +import { getDutchAuctionData, isDutchAuction } from '../../../util/orders'; +import { convertTimeInSecondsToDaysAndHours } from '../../../util/time_utils'; +import { tokenAmountInUnits } from '../../../util/tokens'; +import { Collectible } from '../../../util/types'; +import { Card } from '../../common/card'; + +import { CollectibleDescriptionInnerTitle } from './collectible_description'; + +const PriceChartContainer = styled.div` + display: flex; + justify-content: space-between; + flex-direction: column; + + @media (min-width: ${themeBreakPoints.xl}) { + flex-direction: row; + } +`; + +const PriceChartPriceAndTime = styled.div` + margin-bottom: 25px; + max-width: 150px; + padding-right: 15px; + padding-top: 25px; + + @media (min-width: ${themeBreakPoints.xl}) { + margin-bottom: 0; + } +`; + +export const PriceChartTitle = styled.h5` + color: ${props => props.theme.componentsTheme.cardTitleColor}; + font-size: 14px; + font-weight: 400; + line-height: 1.2; + margin: 0 0 6px; +`; + +export const PriceChartValue = styled.p` + color: #00ae99; + font-size: 14px; + line-height: 1.2; + margin: 0 0 35px; + + &:last-child { + margin-bottom: 0; + } +`; + +const PriceChartValueNeutral = styled(PriceChartValue)` + color: ${props => props.theme.componentsTheme.cardTitleColor}; +`; + +const PriceChartGraphWrapper = styled.div` + flex-grow: 1; + padding-bottom: 15px; + + @media (min-width: ${themeBreakPoints.xl}) { + max-width: 365px; + } +`; + +const PriceChartGraph = styled.div` + background-color: #f5f5f5; + height: 148px; + margin: 0 0 15px; + width: 100%; +`; + +const PriceChartGraphValues = styled.div` + align-items: center; + display: flex; + justify-content: space-between; +`; + +interface Props { + collectible: Collectible; +} + +export const DutchAuctionPriceChartCard = (props: Props) => { + const { collectible } = props; + const { order } = collectible; + if (order === null || !isDutchAuction(order)) { + return null; + } + + const { makerAssetData, expirationTimeSeconds } = order; + const { beginAmount, beginTimeSeconds } = getDutchAuctionData(makerAssetData); + const price = getCollectiblePrice(collectible) as BigNumber; + const { days, hours } = convertTimeInSecondsToDaysAndHours(expirationTimeSeconds.minus(beginTimeSeconds)); + return ( + <Card> + <CollectibleDescriptionInnerTitle>Price Chart</CollectibleDescriptionInnerTitle> + <PriceChartContainer> + <PriceChartPriceAndTime> + <PriceChartTitle>Current Price</PriceChartTitle> + <PriceChartValue>{tokenAmountInUnits(price, ETH_DECIMALS)} ETH</PriceChartValue> + <PriceChartTitle>Time Remaining</PriceChartTitle> + <PriceChartValue>{`${days} Days ${hours} Hrs`}</PriceChartValue> + </PriceChartPriceAndTime> + <PriceChartGraphWrapper> + <PriceChartGraph /> + <PriceChartGraphValues> + <div> + <PriceChartTitle>Start Price</PriceChartTitle> + <PriceChartValueNeutral> + {tokenAmountInUnits(beginAmount, ETH_DECIMALS)} ETH + </PriceChartValueNeutral> + </div> + <div> + <PriceChartTitle>End Price</PriceChartTitle> + <PriceChartValueNeutral> + {tokenAmountInUnits(order.takerAssetAmount, ETH_DECIMALS)} ETH + </PriceChartValueNeutral> + </div> + </PriceChartGraphValues> + </PriceChartGraphWrapper> + </PriceChartContainer> + </Card> + ); +}; diff --git a/src/components/erc721/marketplace/trade_button.tsx b/src/components/erc721/marketplace/trade_button.tsx index 98c614856..90e9c1794 100644 --- a/src/components/erc721/marketplace/trade_button.tsx +++ b/src/components/erc721/marketplace/trade_button.tsx @@ -1,9 +1,13 @@ +import { BigNumber } from '0x.js'; import React from 'react'; import styled, { withTheme } from 'styled-components'; +import { ETH_DECIMALS } from '../../../common/constants'; import { Theme } from '../../../themes/commons'; +import { getCollectiblePrice } from '../../../util/collectibles'; import { getLogger } from '../../../util/logger'; import { isDutchAuction } from '../../../util/orders'; +import { tokenAmountInUnits } from '../../../util/tokens'; import { Collectible } from '../../../util/types'; import { Button as ButtonBase } from '../../common/button'; @@ -69,11 +73,10 @@ export const TradeButtonContainer: React.FC<Props> = ({ onClick = onSell; textColor = theme.componentsTheme.buttonTextColor; } else if (!isOwner && order) { - const price = order.takerAssetAmount; - + const price = getCollectiblePrice(asset) as BigNumber; backgroundColor = theme.componentsTheme.buttonSellBackgroundColor; borderColor = theme.componentsTheme.buttonSellBackgroundColor; - buttonText = `Buy for ${price.toString()} ETH`; + buttonText = `Buy for ${tokenAmountInUnits(price, ETH_DECIMALS)} ETH`; onClick = onBuy; textColor = theme.componentsTheme.buttonTextColor; } else { diff --git a/src/util/collectibles.ts b/src/util/collectibles.ts new file mode 100644 index 000000000..eea7ea202 --- /dev/null +++ b/src/util/collectibles.ts @@ -0,0 +1,25 @@ +import { BigNumber } from '0x.js'; + +import { getDutchAuctionData } from './orders'; +import { todayInSeconds } from './time_utils'; +import { Collectible } from './types'; + +export const getCollectiblePrice = (collectible: Collectible): BigNumber | null => { + const { order } = collectible; + if (order === null) { + return null; + } + + try { + const dutchAcutionData = getDutchAuctionData(order.makerAssetData); + const { beginAmount, beginTimeSeconds } = dutchAcutionData; + const endAmount = order.takerAssetAmount; + const startTimeSeconds = order.expirationTimeSeconds; + // Use y = mx + b (linear function) + const m = endAmount.minus(beginAmount).dividedBy(startTimeSeconds.minus(beginTimeSeconds)); + const b = beginAmount.minus(beginTimeSeconds.multipliedBy(m)); + return m.multipliedBy(todayInSeconds()).plus(b); + } catch (err) { + return order.takerAssetAmount; + } +}; diff --git a/src/util/orders.ts b/src/util/orders.ts index 6ea4ecf2f..c43355ef4 100644 --- a/src/util/orders.ts +++ b/src/util/orders.ts @@ -176,9 +176,13 @@ export const sumTakerAssetFillableOrders = ( }, new BigNumber(0)); }; +export const getDutchAuctionData = (assetData: string) => { + return DutchAuctionWrapper.decodeDutchAuctionData(assetData); +}; + export const isDutchAuction = (order: SignedOrder) => { try { - DutchAuctionWrapper.decodeDutchAuctionData(order.makerAssetData); + getDutchAuctionData(order.makerAssetData); return true; } catch (e) { return false; diff --git a/yarn.lock b/yarn.lock index 6721e4d5e..16394e2b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,16 @@ # yarn lockfile v1 -"0x.js@^6.0.8": - version "6.0.8" - resolved "https://registry.yarnpkg.com/0x.js/-/0x.js-6.0.8.tgz#7c5272cbbe52fe7245341f0acbd0334d61a57f89" +"0x.js@^6.0.9": + version "6.0.9" + resolved "https://registry.yarnpkg.com/0x.js/-/0x.js-6.0.9.tgz#6c798f6912cb0b1fd5c97d5fa087c15a1bee638c" + integrity sha512-QO3GORXWkKLLKPPIU3iymmP9yV+6Wan+tsYynAGmYMUytEQ0EHuC1rWaIV7rnvuPkQA8sHbUV5ZRKYi883Kbkw== dependencies: "@0x/assert" "^2.0.10" "@0x/base-contract" "^5.1.0" - "@0x/contract-wrappers" "^9.1.2" - "@0x/order-utils" "^8.0.2" - "@0x/order-watcher" "^4.0.9" + "@0x/contract-wrappers" "^9.1.3" + "@0x/order-utils" "^8.1.0" + "@0x/order-watcher" "^4.0.10" "@0x/subproviders" "^4.0.6" "@0x/types" "^2.2.2" "@0x/typescript-typings" "^4.2.2" @@ -75,16 +76,17 @@ version "1.5.1" resolved "https://registry.yarnpkg.com/@0x/contract-artifacts/-/contract-artifacts-1.5.1.tgz#6fba56a1d3e2d5d897a75fcfa432e49e2ebb17a7" -"@0x/contract-wrappers@^9.1.2": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@0x/contract-wrappers/-/contract-wrappers-9.1.2.tgz#0f962bf2d77944eed4700c21e7ef417cfcdd0730" +"@0x/contract-wrappers@^9.1.3": + version "9.1.3" + resolved "https://registry.yarnpkg.com/@0x/contract-wrappers/-/contract-wrappers-9.1.3.tgz#64c6789c37df78acbb8d3438b8d0450f7b52bdf9" + integrity sha512-tuQXgEzzWYYD3gK4FthGqgZkVYrTu1u7700c5zRdpxhfhtl14qO6+7FrU+xg63pNQLntfmOUadlEqBLjHFWo1g== dependencies: "@0x/abi-gen-wrappers" "^4.3.0" "@0x/assert" "^2.0.10" "@0x/contract-addresses" "^2.3.3" "@0x/contract-artifacts" "^1.5.1" "@0x/json-schemas" "^3.0.10" - "@0x/order-utils" "^8.0.2" + "@0x/order-utils" "^8.1.0" "@0x/types" "^2.2.2" "@0x/typescript-typings" "^4.2.2" "@0x/utils" "^4.3.3" @@ -99,14 +101,15 @@ lodash "^4.17.11" uuid "^3.3.2" -"@0x/fill-scenarios@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@0x/fill-scenarios/-/fill-scenarios-3.0.8.tgz#db00c23df5b240856f27cfd34e50ac4709d33a70" +"@0x/fill-scenarios@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@0x/fill-scenarios/-/fill-scenarios-3.0.9.tgz#ae855541023c943e91f4bf910ea3c8332b7e2119" + integrity sha512-UP+HWQhbZRcXnYiJUIJOFdJykp7ZBL6CO53Hnbe6rTtdga8yRKyBi5+efJ80YWwrsWGXbsf9os8MuIpKTOdAxw== dependencies: "@0x/abi-gen-wrappers" "^4.3.0" "@0x/base-contract" "^5.1.0" "@0x/contract-artifacts" "^1.5.1" - "@0x/order-utils" "^8.0.2" + "@0x/order-utils" "^8.1.0" "@0x/types" "^2.2.2" "@0x/typescript-typings" "^4.2.2" "@0x/utils" "^4.3.3" @@ -146,19 +149,43 @@ ethers "~4.0.4" lodash "^4.17.11" -"@0x/order-watcher@^4.0.9": - version "4.0.9" - resolved "https://registry.yarnpkg.com/@0x/order-watcher/-/order-watcher-4.0.9.tgz#760fd24043a12d5f4dbd17a79b18770dffbace69" +"@0x/order-utils@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@0x/order-utils/-/order-utils-8.1.0.tgz#5bc9aa76b016f2d64b8fe2b9aade273e29b9afff" + integrity sha512-0ntRi+H2Cn8Pwgcj16yi8Z8evdfE5QFjSNqxbpXH4+OU5OVh2xAFTapvmyEjXn3OxotAJ2GYl4d/f20lUY2ehA== dependencies: "@0x/abi-gen-wrappers" "^4.3.0" "@0x/assert" "^2.0.10" "@0x/base-contract" "^5.1.0" "@0x/contract-addresses" "^2.3.3" "@0x/contract-artifacts" "^1.5.1" - "@0x/contract-wrappers" "^9.1.2" - "@0x/fill-scenarios" "^3.0.8" "@0x/json-schemas" "^3.0.10" - "@0x/order-utils" "^8.0.2" + "@0x/types" "^2.2.2" + "@0x/typescript-typings" "^4.2.2" + "@0x/utils" "^4.3.3" + "@0x/web3-wrapper" "^6.0.6" + "@types/node" "*" + bn.js "^4.11.8" + ethereum-types "^2.1.2" + ethereumjs-abi "0.6.5" + ethereumjs-util "^5.1.1" + ethers "~4.0.4" + lodash "^4.17.11" + +"@0x/order-watcher@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@0x/order-watcher/-/order-watcher-4.0.10.tgz#9749d5947ca5480299809cb7a909aba0ae45b72d" + integrity sha512-GYhCEoyxPYQWse0QhKIYZcnHGUau6WkbD0pXvL/BOl/rWs1vz9EZYfol8a2gIj+429+JRIsO5KK7C65rQ7N3fw== + dependencies: + "@0x/abi-gen-wrappers" "^4.3.0" + "@0x/assert" "^2.0.10" + "@0x/base-contract" "^5.1.0" + "@0x/contract-addresses" "^2.3.3" + "@0x/contract-artifacts" "^1.5.1" + "@0x/contract-wrappers" "^9.1.3" + "@0x/fill-scenarios" "^3.0.9" + "@0x/json-schemas" "^3.0.10" + "@0x/order-utils" "^8.1.0" "@0x/types" "^2.2.2" "@0x/typescript-typings" "^4.2.2" "@0x/utils" "^4.3.3"